diff --git a/.gitignore b/.gitignore index 1d90047..549e00a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,190 +1,33 @@ -# ---> Java -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -replay_pid* - -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Linux -*~ - -# temporary files which can be created if a process still has a handle open of a deleted file -.fuse_hidden* - -# KDE directory preferences -.directory - -# Linux trash folder which might appear on any partition or disk -.Trash-* - -# .nfs files are created when an open file is removed but is still being accessed -.nfs* - -# ---> macOS -# General -.DS_Store -.AppleDouble -.LSOverride - -# Icon must end with two \r -Icon - -# Thumbnails -._* - -# Files that might appear in the root of a volume -.DocumentRevisions-V100 -.fseventsd -.Spotlight-V100 -.TemporaryItems -.Trashes -.VolumeIcon.icns -.com.apple.timemachine.donotpresent - -# Directories potentially created on remote AFP share -.AppleDB -.AppleDesktop -Network Trash Folder -Temporary Items -.apdisk - -# ---> Gradle -.gradle -**/build/ -!src/**/build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar - -# Avoid ignore Gradle wrappper properties -!gradle-wrapper.properties - -# Cache of project -.gradletasknamecache - -# Eclipse Gradle plugin generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) -.classpath - -# ---> Maven +HELP.md target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### STS ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..bf82ff0 Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..ca5ab4b --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# 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 +# +# https://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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/mvnw @@ -0,0 +1,316 @@ +#!/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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,188 @@ +@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 https://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 Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a8d0a52 --- /dev/null +++ b/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.1.0-SNAPSHOT + + + de.arminwolf + finance-analyzer + 0.0.1-SNAPSHOT + finance-analyzer + App to Analyze Excel Exports from Finanzguru + + 17 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-test + test + + + + nz.net.ultraq.thymeleaf + thymeleaf-layout-dialect + 3.2.0 + + + + + org.apache.poi + poi + 5.1.0 + + + + org.apache.poi + poi-ooxml + 5.1.0 + + + + com.fasterxml.jackson.core + jackson-core + 2.13.1 + + + + com.fasterxml.jackson.core + jackson-databind + 2.16.1 + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-csv + 2.13.1 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + diff --git a/src/main/java/de/arminwolf/financeanalyzer/FinanceAnalyzerApplication.java b/src/main/java/de/arminwolf/financeanalyzer/FinanceAnalyzerApplication.java new file mode 100644 index 0000000..f7d288d --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/FinanceAnalyzerApplication.java @@ -0,0 +1,14 @@ +package de.arminwolf.financeanalyzer; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FinanceAnalyzerApplication { + + + public static void main(String[] args) { + SpringApplication.run(FinanceAnalyzerApplication.class, args); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/conf/BankAccount.java b/src/main/java/de/arminwolf/financeanalyzer/conf/BankAccount.java new file mode 100644 index 0000000..9a3a1b1 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/conf/BankAccount.java @@ -0,0 +1,61 @@ +package de.arminwolf.financeanalyzer.conf; + +import java.io.Serializable; +import java.util.Objects; + +public class BankAccount implements Serializable, Comparable { + + private String iban; + private String bankAccountName; + + + public BankAccount() { + } + + + public BankAccount(final String iban, final String bankAccountName) { + this.iban = iban; + this.bankAccountName = bankAccountName; + } + + + @Override + public int compareTo(final BankAccount o) { + return bankAccountName.compareTo(o.bankAccountName); + } + + + public String getIban() { + return iban; + } + + + public void setIban(final String iban) { + this.iban = iban; + } + + + public String getBankAccountName() { + return bankAccountName; + } + + + public void setBankAccountName(final String bankAccountName) { + this.bankAccountName = bankAccountName; + } + + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final BankAccount that = (BankAccount) o; + return Objects.equals(iban, that.iban) && Objects.equals(bankAccountName, that.bankAccountName); + } + + + @Override + public int hashCode() { + return Objects.hash(iban, bankAccountName); + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/conf/Configuration.java b/src/main/java/de/arminwolf/financeanalyzer/conf/Configuration.java new file mode 100644 index 0000000..e41ed8f --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/conf/Configuration.java @@ -0,0 +1,36 @@ +package de.arminwolf.financeanalyzer.conf; + +import java.util.HashSet; +import java.util.Set; + +public class Configuration { + + private Set bankAccounts; + private int numberOfMonths; + + + public Configuration() { + this.numberOfMonths = 1; + this.bankAccounts = new HashSet<>(); + } + + + public Set getBankAccounts() { + return bankAccounts; + } + + + public void setBankAccounts(final Set bankAccounts) { + this.bankAccounts = bankAccounts; + } + + + public int getNumberOfMonths() { + return numberOfMonths; + } + + + public void setNumberOfMonths(final int numberOfMonths) { + this.numberOfMonths = numberOfMonths; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/conf/SpringConfiguration.java b/src/main/java/de/arminwolf/financeanalyzer/conf/SpringConfiguration.java new file mode 100644 index 0000000..513af36 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/conf/SpringConfiguration.java @@ -0,0 +1,15 @@ +package de.arminwolf.financeanalyzer.conf; + +import nz.net.ultraq.thymeleaf.layoutdialect.LayoutDialect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SpringConfiguration { + + @Bean + public LayoutDialect layoutDialect() { + return new LayoutDialect(); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/controller/ConverterController.java b/src/main/java/de/arminwolf/financeanalyzer/controller/ConverterController.java new file mode 100644 index 0000000..52448be --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/controller/ConverterController.java @@ -0,0 +1,111 @@ +package de.arminwolf.financeanalyzer.controller; + +import de.arminwolf.financeanalyzer.conf.BankAccount; +import de.arminwolf.financeanalyzer.conf.Configuration; +import de.arminwolf.financeanalyzer.service.CacheService; +import de.arminwolf.financeanalyzer.service.ConverterService; +import de.arminwolf.financeanalyzer.service.FileService; +import de.arminwolf.financeanalyzer.util.Constants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +@Controller +@RequestMapping("/data") +public class ConverterController { + + @Autowired + ConverterService service; + + @Autowired + CacheService cacheService; + + @Autowired + FileService fileService; + + + @PostMapping("/convert") + public String convert(RedirectAttributes redirectAttributes, @RequestParam("file") MultipartFile file) { + try (InputStream in = file.getInputStream()) { + Optional json = service.convert(in); + if (json.isPresent()) { + File workingDirectory = fileService.getOrCreateWorkingDirectory(); + Path tempFile = Files.createFile(Paths.get(workingDirectory.getPath(), "finance-analyzer.json")); + Path path = Files.writeString(tempFile, json.get(), StandardCharsets.UTF_8); + + cacheService.put("currentJsonFile", path.toAbsolutePath().toFile().toString()); + Configuration configuration = new Configuration(); + Set bankAccounts = + Arrays.stream(fileService.getJsonFile()) + .map(t -> new BankAccount(t.getReferenzkonto(), t.getNameReferenzkonto())) + .collect(Collectors.toSet()); + + configuration.setBankAccounts(bankAccounts); + fileService.saveConfiguration(configuration); + redirectAttributes.addFlashAttribute("result", "Converted successfully!"); + return "redirect:/data/success"; + } else { + redirectAttributes.addFlashAttribute("result", "Error while converting"); + return "redirect:/data/error"; + } + + } catch (IOException e) { + redirectAttributes.addFlashAttribute("result", "Error while converting"); + return "redirect:/data/error"; + } + } + + + @PostMapping("/load") + public String convert(RedirectAttributes redirectAttributes, + @RequestParam("account") String account, + @RequestParam("month") String month) { + redirectAttributes.addFlashAttribute("result", "Converted successfully!"); + redirectAttributes.addFlashAttribute("account", account); + redirectAttributes.addFlashAttribute("month", month); + System.out.println("[ Load Method ] Account: " + account); + System.out.println("[ Load Method ] Month: " + month); + return "redirect:/data/success"; + } + + + @GetMapping("/templates/error") + public String handleError(@ModelAttribute("result") String result, Model model) { + model.addAttribute("result", result); + return Constants.ERROR; + } + + + @GetMapping("/success") + public String handleSuccess(RedirectAttributes redirectAttributes, + @ModelAttribute("result") String result, + @ModelAttribute("account") String account, + @ModelAttribute("month") String month, + Model model) { + redirectAttributes.addFlashAttribute("result", "Converted successfully!"); + redirectAttributes.addFlashAttribute("account", account); + redirectAttributes.addFlashAttribute("month", month); + redirectAttributes.addFlashAttribute("result", result); + return "redirect:/".concat(Constants.REPORTS).concat("/"); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/controller/CustomErrorController.java b/src/main/java/de/arminwolf/financeanalyzer/controller/CustomErrorController.java new file mode 100644 index 0000000..97b384f --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/controller/CustomErrorController.java @@ -0,0 +1,31 @@ +package de.arminwolf.financeanalyzer.controller; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.boot.web.servlet.error.ErrorController; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class CustomErrorController implements ErrorController { + + @RequestMapping("/error") + public String handleError(HttpServletRequest request) { + Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + + if (status != null) { + int statusCode = Integer.parseInt(status.toString()); + + if(statusCode == HttpStatus.NOT_FOUND.value()) { + System.out.println(request.getRequestURL()); + return "/error/404"; + } + else if(statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) { + return "/error/500"; + } + } + + return "/error"; + } +} \ No newline at end of file diff --git a/src/main/java/de/arminwolf/financeanalyzer/controller/FileController.java b/src/main/java/de/arminwolf/financeanalyzer/controller/FileController.java new file mode 100644 index 0000000..f54207c --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/controller/FileController.java @@ -0,0 +1,38 @@ +package de.arminwolf.financeanalyzer.controller; + +import de.arminwolf.financeanalyzer.service.CacheService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Objects; + +@RestController +@RequestMapping("/file") +public class FileController { + + + @Autowired + CacheService cacheService; + + + @RequestMapping(value = "/current", method = RequestMethod.GET, produces = "application/json") + public ResponseEntity get() throws IOException { + + if (Objects.nonNull(cacheService.get("currentJsonFile"))) { + String jsonFile = Files.readString(Paths.get(cacheService.get("currentJsonFile").toString())); + if (Objects.nonNull(jsonFile) && !jsonFile.isEmpty()) { + return ResponseEntity.ok(jsonFile); + } + } + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("No persistent JSON found. Upload a new one."); + + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/controller/MainController.java b/src/main/java/de/arminwolf/financeanalyzer/controller/MainController.java new file mode 100644 index 0000000..edbb33b --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/controller/MainController.java @@ -0,0 +1,55 @@ +package de.arminwolf.financeanalyzer.controller; + +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import de.arminwolf.financeanalyzer.util.UrlUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Objects; +import java.util.Optional; + +import static de.arminwolf.financeanalyzer.util.Constants.INDEX; + +@Controller +public class MainController { + + + @GetMapping("/") + public String get(Model model, HttpServletRequest request) { + RestTemplate restTemplate = new RestTemplate(); + + try { + final Optional response = getTransactions(request, restTemplate); + if (response.isEmpty()) { + return "redirect:/upload/"; + } else { + // Verarbeiten Sie hier die Antwort + model.addAttribute("data", response.get()); + return INDEX; + } + } catch (Exception e) { + e.printStackTrace(System.err); + return "redirect:/upload"; + } + + } + + private static Optional getTransactions(final HttpServletRequest request, final RestTemplate restTemplate) { + try { + TransactionDAO[] forObject = restTemplate.getForObject(UrlUtil.getRelativeUrl(request, "/file/current"), TransactionDAO[].class); + if (Objects.isNull(forObject)) { + return Optional.empty(); + } else { + return Optional.of(forObject); + } + } catch (RestClientException e) { + return Optional.empty(); + } + + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/controller/ReportController.java b/src/main/java/de/arminwolf/financeanalyzer/controller/ReportController.java new file mode 100644 index 0000000..2aeecf4 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/controller/ReportController.java @@ -0,0 +1,57 @@ +package de.arminwolf.financeanalyzer.controller; + +import de.arminwolf.financeanalyzer.conf.Configuration; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import de.arminwolf.financeanalyzer.dao.ReportDAO; +import de.arminwolf.financeanalyzer.dao.TableDataDAO; +import de.arminwolf.financeanalyzer.service.CacheService; +import de.arminwolf.financeanalyzer.service.ReportService; +import de.arminwolf.financeanalyzer.util.Constants; +import de.arminwolf.financeanalyzer.util.DateUtil; +import de.arminwolf.financeanalyzer.util.UrlUtil; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +@Controller +@RequestMapping("/reports") +public class ReportController { + + @Autowired + private ReportService reportService; + + @GetMapping("/") + public String reports(@ModelAttribute("account") String iban, + @ModelAttribute("month") String month, + Model model, HttpServletRequest request) { + RestTemplate restTemplate = new RestTemplate(); + if (Objects.isNull(iban) || iban.isEmpty()) { + return "redirect:/upload/"; + } + + try { + final String fileURL = UrlUtil.getRelativeUrl(request, "/file/current"); + final TransactionDAO[] response = restTemplate.getForObject(fileURL, TransactionDAO[].class); + if (Objects.isNull(response)) { + return "redirect:/upload"; + } else { + reportService.createReport(response, month, iban, model); + return Constants.REPORTS; + } + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + return "redirect:/upload/"; + } + + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/controller/UploadController.java b/src/main/java/de/arminwolf/financeanalyzer/controller/UploadController.java new file mode 100644 index 0000000..e5e4ba9 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/controller/UploadController.java @@ -0,0 +1,38 @@ +package de.arminwolf.financeanalyzer.controller; + +import de.arminwolf.financeanalyzer.conf.Configuration; +import de.arminwolf.financeanalyzer.service.CacheService; +import de.arminwolf.financeanalyzer.service.FileService; +import de.arminwolf.financeanalyzer.service.ReportService; +import de.arminwolf.financeanalyzer.util.Constants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.mvc.support.RedirectAttributesModelMap; + +@Controller +@RequestMapping("/upload") +public class UploadController { + @Autowired + FileService fileService; + + @Autowired + CacheService cacheService; + + + @GetMapping("/") + public String upload(Model model) { + final RedirectAttributes redirectAttributes = new RedirectAttributesModelMap(); + fileService.getJsonFileName().ifPresent(jsonFileName -> { + cacheService.put("currentJsonFile", jsonFileName); + cacheService.put("configuration", fileService.getConfiguration()); + model.addAttribute("foundJsonFile", jsonFileName); + model.addAttribute("accounts", ((Configuration) cacheService.get("configuration")).getBankAccounts()); + }); + + return Constants.INDEX; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/ReportDAO.java b/src/main/java/de/arminwolf/financeanalyzer/dao/ReportDAO.java new file mode 100644 index 0000000..91eb95d --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/ReportDAO.java @@ -0,0 +1,151 @@ +package de.arminwolf.financeanalyzer.dao; + +import de.arminwolf.financeanalyzer.dao.charts.model.HighChartDAO; +import de.arminwolf.financeanalyzer.dao.charts.model.Series; +import de.arminwolf.financeanalyzer.dao.charts.factories.BubbleChartFactory; +import de.arminwolf.financeanalyzer.dao.charts.factories.ColumnChartFactory; +import de.arminwolf.financeanalyzer.dao.charts.factories.LineChartFactory; +import de.arminwolf.financeanalyzer.dao.model.OutputData; +import de.arminwolf.financeanalyzer.util.NumberUtil; +import de.arminwolf.financeanalyzer.util.OrderDateStringComparator; +import org.apache.commons.math3.util.Pair; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class ReportDAO { + + + private HighChartDAO cashFlowChart; + private HighChartDAO accountBalanceLineChart; + private HighChartDAO transactionCategoriesBubbleChart; + + private Set standingOutputData; + private Set notStandingOutputData; + private List incomeList; + + + public ReportDAO() { + this.standingOutputData = new HashSet<>(); + this.notStandingOutputData = new HashSet<>(); + this.incomeList = new ArrayList<>(); + } + + + public Set getStandingOrders() { + return this.standingOutputData; + } + + + public List getSortedStandingOrders() { + return this.standingOutputData.stream().sorted(new OrderDateStringComparator()).collect(Collectors.toList()); + } + + + public Float getStandingOrdersAmount() { + return NumberUtil.round(getStandingOrders().stream().map(OutputData::getAmountAsFloat).reduce(0f, Float::sum)); + } + + + public Float getNotStandingOrdersAmount() { + return NumberUtil.round(getNotStandingOrders().stream().map(OutputData::getAmountAsFloat).reduce(0f, Float::sum)); + } + + + public HighChartDAO getTransactionCategoriesBubbleChart() { + return transactionCategoriesBubbleChart; + } + + + public void setIncomeList(final List incomes) { + this.incomeList = incomes.stream() + .map(t -> new OutputData(t.getVerwendungszweck(), t)) + .sorted(new OrderDateStringComparator()) + .collect(Collectors.toList()); + } + + + public List getIncomeList() { + return incomeList; + } + + + public void setNotStandingOrders(final List notStandingOrders) { + Set list = new HashSet<>(); + notStandingOrders.forEach(order -> list.add(new OutputData(order))); + this.notStandingOutputData = list; + } + + + public final Set getNotStandingOrders() { + return notStandingOutputData; + } + + + public final List getSortedNotStandingOrders() { + return notStandingOutputData.stream().sorted(new OrderDateStringComparator()).collect(Collectors.toList()); + } + + + public Float getTotalExpense() { + return getStandingOrdersAmount() + getNotStandingOrdersAmount(); + } + + + public void setTransactionCategoriesBubbleChart(final List transactions) { + this.transactionCategoriesBubbleChart = BubbleChartFactory.createBubbleChart("Kateogrien der Transaktionen", transactions); + } + + + public void setAccountBalanceLineChart(final List> pairList) { + String title = "Verlauf des Kontostandes"; + String chartTitle = "Kontostand in EUR"; + List series = new ArrayList<>(); + Series data = new Series(); + data.setData(pairList.stream().map(e -> Float.parseFloat(e.getValue())).collect(Collectors.toList())); + series.add(data); + + this.accountBalanceLineChart = LineChartFactory.createLineChart(title, chartTitle, pairList.stream().map(Pair::getKey).collect(Collectors.toList()), series); + } + + + public HighChartDAO getAccountBalanceLineChart() { + return accountBalanceLineChart; + } + + + public void setCashFlowChart(final Map> ausgaben, final Map> einnahmen) { + this.cashFlowChart = + ColumnChartFactory.createColumnChart("Cashflow", + "Einnahmen und Ausgaben", + ausgaben, "Ausgaben", + einnahmen, "Einnahmen"); + } + + + public HighChartDAO getCashFlowChart() { + return cashFlowChart; + } + + + public Float getIncome() { + return this.incomeList.stream().map(OutputData::getAmountAsFloat).reduce(0f, Float::sum); + } + + + public Float getDiff() { + return NumberUtil.round(Math.abs(getIncome()) - Math.abs(getTotalExpense())); + } + + + public void setStandingOrders(final Collection orders) { + Set list = new HashSet<>(); + orders.forEach(order -> list.add(new OutputData(order))); + this.standingOutputData = list; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/TableDataDAO.java b/src/main/java/de/arminwolf/financeanalyzer/dao/TableDataDAO.java new file mode 100644 index 0000000..fc05802 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/TableDataDAO.java @@ -0,0 +1,37 @@ +package de.arminwolf.financeanalyzer.dao; + +import java.util.List; + +public class TableDataDAO { + + private List headers; + private List> rows; + + + public List getHeaders() { + return headers; + } + + + public void setHeaders(final List headers) { + this.headers = headers; + } + + + public List> getRows() { + return rows; + } + + + public void setRows(final List> rows) { + this.rows = rows; + } + + + + public static class TableDataFactory { + public static TableDataDAO createTableDataDTO() { + return new TableDataDAO(); + } + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/TransactionDAO.java b/src/main/java/de/arminwolf/financeanalyzer/dao/TransactionDAO.java new file mode 100644 index 0000000..bf3b882 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/TransactionDAO.java @@ -0,0 +1,555 @@ +package de.arminwolf.financeanalyzer.dao; + + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "mandatsreferenz", + "analyse-vom-frei-verfuegbaren-einkommen-ausgeschlossen", + "analyse-vertrag", + "e-ref", + "analyse-hauptkategorie", + "analyse-umsatzart", + "analyse-woche", + "analyse-monat", + "analyse-quartal", + "kontostand", + "analyse-vertrags-id", + "waehrung", + "verwendungszweck", + "buchungstag", + "analyse-vertragsturnus", + "analyse-betrag", + "betrag", + "name-referenzkonto", + "beguenstigter/auftraggeber", + "analyse-umbuchung", + "iban-beguenstigter/auftraggeber", + "referenzkonto", + "analyse-unterkategorie", + "analyse-jahr", + "glaeubiger-id" +}) +public class TransactionDAO { + + @JsonProperty("mandatsreferenz") + private String mandatsreferenz; + @JsonProperty("analyse-vom-frei-verfuegbaren-einkommen-ausgeschlossen") + private String analyseVomFreiVerfuegbarenEinkommenAusgeschlossen; + @JsonProperty("analyse-vertrag") + private String analyseVertrag; + @JsonProperty("e-ref") + private String eRef; + @JsonProperty("analyse-hauptkategorie") + private String analyseHauptkategorie; + @JsonProperty("analyse-umsatzart") + private String analyseUmsatzart; + @JsonProperty("analyse-woche") + private String analyseWoche; + @JsonProperty("analyse-monat") + private String analyseMonat; + @JsonProperty("analyse-quartal") + private String analyseQuartal; + @JsonProperty("kontostand") + private String kontostand; + @JsonProperty("analyse-vertrags-id") + private String analyseVertragsId; + @JsonProperty("waehrung") + private String waehrung; + @JsonProperty("verwendungszweck") + private String verwendungszweck; + @JsonProperty("buchungstag") + private String buchungstag; + @JsonProperty("analyse-vertragsturnus") + private String analyseVertragsturnus; + @JsonProperty("analyse-betrag") + private String analyseBetrag; + @JsonProperty("betrag") + private String betrag; + @JsonProperty("name-referenzkonto") + private String nameReferenzkonto; + @JsonProperty("beguenstigter/auftraggeber") + private String beguenstigterAuftraggeber; + @JsonProperty("analyse-umbuchung") + private String analyseUmbuchung; + @JsonProperty("iban-beguenstigter/auftraggeber") + private String ibanBeguenstigterAuftraggeber; + @JsonProperty("referenzkonto") + private String referenzkonto; + @JsonProperty("analyse-unterkategorie") + private String analyseUnterkategorie; + @JsonProperty("analyse-jahr") + private String analyseJahr; + @JsonProperty("glaeubiger-id") + private String glaeubigerId; + @JsonIgnore + private final Map additionalProperties = new LinkedHashMap(); + + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + + @JsonProperty("analyse-betrag") + public String getAnalyseBetrag() { + return analyseBetrag; + } + + + @JsonProperty("analyse-hauptkategorie") + public String getAnalyseHauptkategorie() { + return analyseHauptkategorie; + } + + + @JsonProperty("analyse-jahr") + public String getAnalyseJahr() { + return analyseJahr; + } + + + @JsonProperty("analyse-monat") + public String getAnalyseMonat() { + return analyseMonat; + } + + + @JsonProperty("analyse-quartal") + public String getAnalyseQuartal() { + return analyseQuartal; + } + + + @JsonProperty("analyse-umbuchung") + public String getAnalyseUmbuchung() { + return analyseUmbuchung; + } + + + @JsonProperty("analyse-umsatzart") + public String getAnalyseUmsatzart() { + return analyseUmsatzart; + } + + + @JsonProperty("analyse-unterkategorie") + public String getAnalyseUnterkategorie() { + return analyseUnterkategorie; + } + + + @JsonProperty("analyse-vertrag") + public String getAnalyseVertrag() { + return analyseVertrag; + } + + + @JsonProperty("analyse-vertrags-id") + public String getAnalyseVertragsId() { + return analyseVertragsId; + } + + + @JsonProperty("analyse-vertragsturnus") + public String getAnalyseVertragsturnus() { + return analyseVertragsturnus; + } + + + @JsonProperty("analyse-vom-frei-verfuegbaren-einkommen-ausgeschlossen") + public String getAnalyseVomFreiVerfuegbarenEinkommenAusgeschlossen() { + return analyseVomFreiVerfuegbarenEinkommenAusgeschlossen; + } + + + @JsonProperty("analyse-woche") + public String getAnalyseWoche() { + return analyseWoche; + } + + + @JsonProperty("beguenstigter/auftraggeber") + public String getBeguenstigterAuftraggeber() { + return beguenstigterAuftraggeber; + } + + + @JsonProperty("betrag") + public String getBetrag() { + return betrag; + } + + + public float getBetragAsFloat() { + return Float.parseFloat(betrag); + } + + + @JsonProperty("buchungstag") + public String getBuchungstag() { + return buchungstag; + } + + + @JsonProperty("glaeubiger-id") + public String getGlaeubigerId() { + return glaeubigerId; + } + + + @JsonProperty("iban-beguenstigter/auftraggeber") + public String getIbanBeguenstigterAuftraggeber() { + return ibanBeguenstigterAuftraggeber; + } + + + @JsonProperty("kontostand") + public String getKontostand() { + return kontostand; + } + + + @JsonProperty("mandatsreferenz") + public String getMandatsreferenz() { + return mandatsreferenz; + } + + + @JsonProperty("name-referenzkonto") + public String getNameReferenzkonto() { + return nameReferenzkonto; + } + + + @JsonProperty("referenzkonto") + public String getReferenzkonto() { + return referenzkonto; + } + + + @JsonProperty("verwendungszweck") + public String getVerwendungszweck() { + return verwendungszweck; + } + + + @JsonProperty("waehrung") + public String getWaehrung() { + return waehrung; + } + + + @JsonProperty("e-ref") + public String geteRef() { + return eRef; + } + + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + + @JsonProperty("analyse-betrag") + public void setAnalyseBetrag(String analyseBetrag) { + this.analyseBetrag = analyseBetrag; + } + + + @JsonProperty("analyse-hauptkategorie") + public void setAnalyseHauptkategorie(String analyseHauptkategorie) { + this.analyseHauptkategorie = analyseHauptkategorie; + } + + + @JsonProperty("analyse-jahr") + public void setAnalyseJahr(String analyseJahr) { + this.analyseJahr = analyseJahr; + } + + + @JsonProperty("analyse-monat") + public void setAnalyseMonat(String analyseMonat) { + this.analyseMonat = analyseMonat; + } + + + @JsonProperty("analyse-quartal") + public void setAnalyseQuartal(String analyseQuartal) { + this.analyseQuartal = analyseQuartal; + } + + + @JsonProperty("analyse-umbuchung") + public void setAnalyseUmbuchung(String analyseUmbuchung) { + this.analyseUmbuchung = analyseUmbuchung; + } + + + @JsonProperty("analyse-umsatzart") + public void setAnalyseUmsatzart(String analyseUmsatzart) { + this.analyseUmsatzart = analyseUmsatzart; + } + + + @JsonProperty("analyse-unterkategorie") + public void setAnalyseUnterkategorie(String analyseUnterkategorie) { + this.analyseUnterkategorie = analyseUnterkategorie; + } + + + @JsonProperty("analyse-vertrag") + public void setAnalyseVertrag(String analyseVertrag) { + this.analyseVertrag = analyseVertrag; + } + + + @JsonProperty("analyse-vertrags-id") + public void setAnalyseVertragsId(String analyseVertragsId) { + this.analyseVertragsId = analyseVertragsId; + } + + + @JsonProperty("analyse-vertragsturnus") + public void setAnalyseVertragsturnus(String analyseVertragsturnus) { + this.analyseVertragsturnus = analyseVertragsturnus; + } + + + @JsonProperty("analyse-vom-frei-verfuegbaren-einkommen-ausgeschlossen") + public void setAnalyseVomFreiVerfuegbarenEinkommenAusgeschlossen(String analyseVomFreiVerfuegbarenEinkommenAusgeschlossen) { + this.analyseVomFreiVerfuegbarenEinkommenAusgeschlossen = analyseVomFreiVerfuegbarenEinkommenAusgeschlossen; + } + + + @JsonProperty("analyse-woche") + public void setAnalyseWoche(String analyseWoche) { + this.analyseWoche = analyseWoche; + } + + + @JsonProperty("beguenstigter/auftraggeber") + public void setBeguenstigterAuftraggeber(String beguenstigterAuftraggeber) { + if (beguenstigterAuftraggeber.contains("Abbuchung")) { + this.beguenstigterAuftraggeber = "Sparkasse Münsterland Ost"; + } else { + this.beguenstigterAuftraggeber = beguenstigterAuftraggeber; + } + + } + + + @JsonProperty("betrag") + public void setBetrag(String betrag) { + this.betrag = betrag; + } + + + @JsonProperty("buchungstag") + public void setBuchungstag(String buchungstag) { + this.buchungstag = buchungstag; + } + + + @JsonProperty("glaeubiger-id") + public void setGlaeubigerId(String glaeubigerId) { + this.glaeubigerId = glaeubigerId; + } + + + @JsonProperty("iban-beguenstigter/auftraggeber") + public void setIbanBeguenstigterAuftraggeber(String ibanBeguenstigterAuftraggeber) { + this.ibanBeguenstigterAuftraggeber = ibanBeguenstigterAuftraggeber; + } + + + @JsonProperty("kontostand") + public void setKontostand(String kontostand) { + this.kontostand = kontostand; + } + + + @JsonProperty("mandatsreferenz") + public void setMandatsreferenz(String mandatsreferenz) { + this.mandatsreferenz = mandatsreferenz; + } + + + @JsonProperty("name-referenzkonto") + public void setNameReferenzkonto(String nameReferenzkonto) { + this.nameReferenzkonto = nameReferenzkonto; + } + + + @JsonProperty("referenzkonto") + public void setReferenzkonto(String referenzkonto) { + this.referenzkonto = referenzkonto; + } + + + @JsonProperty("verwendungszweck") + public void setVerwendungszweck(String verwendungszweck) { + this.verwendungszweck = verwendungszweck; + } + + + @JsonProperty("waehrung") + public void setWaehrung(String waehrung) { + this.waehrung = waehrung; + } + + + @JsonProperty("e-ref") + public void seteRef(String eRef) { + this.eRef = eRef; + } + + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(TransactionDAO.class.getName()).append('@').append(Integer.toHexString(System.identityHashCode(this))).append('['); + sb.append("mandatsreferenz"); + sb.append('='); + sb.append(((this.mandatsreferenz == null) ? "" : this.mandatsreferenz)); + sb.append(','); + sb.append("analyseVomFreiVerfuegbarenEinkommenAusgeschlossen"); + sb.append('='); + sb.append(((this.analyseVomFreiVerfuegbarenEinkommenAusgeschlossen == null) ? "" : this.analyseVomFreiVerfuegbarenEinkommenAusgeschlossen)); + sb.append(','); + sb.append("analyseVertrag"); + sb.append('='); + sb.append(((this.analyseVertrag == null) ? "" : this.analyseVertrag)); + sb.append(','); + sb.append("eRef"); + sb.append('='); + sb.append(((this.eRef == null) ? "" : this.eRef)); + sb.append(','); + sb.append("analyseHauptkategorie"); + sb.append('='); + sb.append(((this.analyseHauptkategorie == null) ? "" : this.analyseHauptkategorie)); + sb.append(','); + sb.append("analyseUmsatzart"); + sb.append('='); + sb.append(((this.analyseUmsatzart == null) ? "" : this.analyseUmsatzart)); + sb.append(','); + sb.append("analyseWoche"); + sb.append('='); + sb.append(((this.analyseWoche == null) ? "" : this.analyseWoche)); + sb.append(','); + sb.append("analyseMonat"); + sb.append('='); + sb.append(((this.analyseMonat == null) ? "" : this.analyseMonat)); + sb.append(','); + sb.append("analyseQuartal"); + sb.append('='); + sb.append(((this.analyseQuartal == null) ? "" : this.analyseQuartal)); + sb.append(','); + sb.append("kontostand"); + sb.append('='); + sb.append(((this.kontostand == null) ? "" : this.kontostand)); + sb.append(','); + sb.append("analyseVertragsId"); + sb.append('='); + sb.append(((this.analyseVertragsId == null) ? "" : this.analyseVertragsId)); + sb.append(','); + sb.append("waehrung"); + sb.append('='); + sb.append(((this.waehrung == null) ? "" : this.waehrung)); + sb.append(','); + sb.append("verwendungszweck"); + sb.append('='); + sb.append(((this.verwendungszweck == null) ? "" : this.verwendungszweck)); + sb.append(','); + sb.append("buchungstag"); + sb.append('='); + sb.append(((this.buchungstag == null) ? "" : this.buchungstag)); + sb.append(','); + sb.append("analyseVertragsturnus"); + sb.append('='); + sb.append(((this.analyseVertragsturnus == null) ? "" : this.analyseVertragsturnus)); + sb.append(','); + sb.append("analyseBetrag"); + sb.append('='); + sb.append(((this.analyseBetrag == null) ? "" : this.analyseBetrag)); + sb.append(','); + sb.append("betrag"); + sb.append('='); + sb.append(((this.betrag == null) ? "" : this.betrag)); + sb.append(','); + sb.append("nameReferenzkonto"); + sb.append('='); + sb.append(((this.nameReferenzkonto == null) ? "" : this.nameReferenzkonto)); + sb.append(','); + sb.append("beguenstigterAuftraggeber"); + sb.append('='); + sb.append(((this.beguenstigterAuftraggeber == null) ? "" : this.beguenstigterAuftraggeber)); + sb.append(','); + sb.append("analyseUmbuchung"); + sb.append('='); + sb.append(((this.analyseUmbuchung == null) ? "" : this.analyseUmbuchung)); + sb.append(','); + sb.append("ibanBeguenstigterAuftraggeber"); + sb.append('='); + sb.append(((this.ibanBeguenstigterAuftraggeber == null) ? "" : this.ibanBeguenstigterAuftraggeber)); + sb.append(','); + sb.append("referenzkonto"); + sb.append('='); + sb.append(((this.referenzkonto == null) ? "" : this.referenzkonto)); + sb.append(','); + sb.append("analyseUnterkategorie"); + sb.append('='); + sb.append(((this.analyseUnterkategorie == null) ? "" : this.analyseUnterkategorie)); + sb.append(','); + sb.append("analyseJahr"); + sb.append('='); + sb.append(((this.analyseJahr == null) ? "" : this.analyseJahr)); + sb.append(','); + sb.append("glaeubigerId"); + sb.append('='); + sb.append(((this.glaeubigerId == null) ? "" : this.glaeubigerId)); + sb.append(','); + sb.append("additionalProperties"); + sb.append('='); + sb.append(((this.additionalProperties == null) ? "" : this.additionalProperties)); + sb.append(','); + if (sb.charAt((sb.length() - 1)) == ',') { + sb.setCharAt((sb.length() - 1), ']'); + } else { + sb.append(']'); + } + return sb.toString(); + } + + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final TransactionDAO that = (TransactionDAO) o; + return Objects.equals(referenzkonto, that.referenzkonto) + && Objects.equals(analyseBetrag, that.analyseBetrag) + && Objects.equals(verwendungszweck, that.verwendungszweck) + && Objects.equals(betrag, that.betrag) + && Objects.equals(ibanBeguenstigterAuftraggeber, that.ibanBeguenstigterAuftraggeber); + } + + + @Override + public int hashCode() { + return Objects.hash(referenzkonto, verwendungszweck, analyseBetrag, betrag, ibanBeguenstigterAuftraggeber); + } +} \ No newline at end of file diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/BubbleChartFactory.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/BubbleChartFactory.java new file mode 100644 index 0000000..2ce3302 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/BubbleChartFactory.java @@ -0,0 +1,60 @@ +package de.arminwolf.financeanalyzer.dao.charts.factories; + +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import de.arminwolf.financeanalyzer.dao.charts.model.Chart; +import de.arminwolf.financeanalyzer.dao.charts.model.HighChartDAO; +import de.arminwolf.financeanalyzer.dao.charts.model.Packedbubble; +import de.arminwolf.financeanalyzer.dao.charts.model.PlotOptions; +import de.arminwolf.financeanalyzer.dao.charts.model.Series; +import de.arminwolf.financeanalyzer.dao.charts.model.SeriesElement; +import de.arminwolf.financeanalyzer.dao.charts.model.Title; +import de.arminwolf.financeanalyzer.dao.charts.model.ToolTip; +import de.arminwolf.financeanalyzer.util.RandomColorUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +public class BubbleChartFactory { + + public static HighChartDAO createBubbleChart( + final String pTitle, + final List transactions) { + HighChartDAO highChartDAO = new HighChartDAO(); + highChartDAO.setTitle(new Title(pTitle)); + + highChartDAO.setChart(new Chart("packedbubble", "75%")); + + List series = new ArrayList<>(); + List colors = new ArrayList<>(); + + AtomicInteger min = new AtomicInteger(0); + AtomicInteger max = new AtomicInteger(0); + + transactions.stream() + .collect(Collectors.groupingBy(TransactionDAO::getAnalyseHauptkategorie)) + .forEach((key, value) -> { + Series data = new Series(); + data.setName(key); + Float reduce = value.stream().reduce(0f, (a, b) -> a + (-1.0f * b.getBetragAsFloat()), Float::sum); + min.set(Math.min(min.get(), reduce.intValue())); + max.set(Math.max(max.get(), reduce.intValue())); + data.setComplexData(value.stream().map(b -> new SeriesElement(b.getBeguenstigterAuftraggeber(), -1.0f * b.getBetragAsFloat())).collect(Collectors.toList())); + series.add(data); + colors.add(RandomColorUtil.getRandomColor()); + }); + + PlotOptions plotOptions = new PlotOptions(); + plotOptions.setPackedbubble(new Packedbubble()); + plotOptions.getPackedbubble().setzMax(max.get()); + plotOptions.getPackedbubble().setzMin(min.get()); + plotOptions.getPackedbubble().getDataLabels().getFilter().setValue(Double.valueOf(0.2 * max.get()).intValue()); + highChartDAO.setPlotOptions(plotOptions); + + highChartDAO.setTooltip(new ToolTip("{point.name}: {point.value} €", true)); + highChartDAO.setColors(colors); + highChartDAO.setSeries(series); + return highChartDAO; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/ColumnChartFactory.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/ColumnChartFactory.java new file mode 100644 index 0000000..d996db5 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/ColumnChartFactory.java @@ -0,0 +1,74 @@ +package de.arminwolf.financeanalyzer.dao.charts.factories; + +import de.arminwolf.financeanalyzer.dao.charts.model.Chart; +import de.arminwolf.financeanalyzer.dao.charts.model.Column; +import de.arminwolf.financeanalyzer.dao.charts.model.HighChartDAO; +import de.arminwolf.financeanalyzer.dao.charts.model.PlotOptions; +import de.arminwolf.financeanalyzer.dao.charts.model.Series; +import de.arminwolf.financeanalyzer.dao.charts.model.Title; +import de.arminwolf.financeanalyzer.dao.charts.model.XAxis; +import de.arminwolf.financeanalyzer.dao.charts.model.YAxis; +import de.arminwolf.financeanalyzer.util.RandomColorUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ColumnChartFactory { + + public static HighChartDAO createColumnChart( + final String pTitle, + final String pChartTitle, + final Map> ausgaben, + final String pAusgabenTitle, + final Map> einnahmen, + final String pEinnahmenTitle) { + + HighChartDAO chart = new HighChartDAO(); + + PlotOptions plotOptions = new PlotOptions(); + Column column = new Column(); + //column.setStacking("normal"); + plotOptions.setColumn(column); + chart.setPlotOptions(plotOptions); + + chart.setTitle(new Title(pTitle)); + + XAxis xAxis = new XAxis(); + xAxis.setCategories(List.of("Einname", "Ausgabe")); + chart.setXAxis(xAxis); + + YAxis yAxis = new YAxis(); + yAxis.setMin(0); + yAxis.setTitle(new Title(pChartTitle)); + chart.setYAxis(yAxis); + + chart.setChart(new Chart("column")); + + Series einnahmenSeries = new Series(); + einnahmenSeries.setStack(1); + einnahmenSeries.setName(pEinnahmenTitle); + einnahmenSeries.setData(einnahmen.values() + .stream() + .map(floats -> floats.stream().reduce(0.0f, Float::sum)) + .collect(Collectors.toList())); + + + Series ausgabenSeries = new Series(); + ausgabenSeries.setStack(0); + ausgabenSeries.setName(pAusgabenTitle); + ausgabenSeries.setData(ausgaben.values() + .stream() + .map(floats -> floats.stream().map(Math::abs).reduce(0.0f, Float::sum)) + .collect(Collectors.toList())); + + chart.setColors(List.of(RandomColorUtil.getRandomColor(), RandomColorUtil.getRandomColor())); + + List series = new ArrayList<>(); + series.add(einnahmenSeries); + series.add(ausgabenSeries); + chart.setSeries(series); + return chart; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/LineChartFactory.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/LineChartFactory.java new file mode 100644 index 0000000..94da89e --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/factories/LineChartFactory.java @@ -0,0 +1,42 @@ +package de.arminwolf.financeanalyzer.dao.charts.factories; + +import de.arminwolf.financeanalyzer.dao.charts.model.Chart; +import de.arminwolf.financeanalyzer.dao.charts.model.HighChartDAO; +import de.arminwolf.financeanalyzer.dao.charts.model.Series; +import de.arminwolf.financeanalyzer.dao.charts.model.Title; +import de.arminwolf.financeanalyzer.dao.charts.model.XAxis; +import de.arminwolf.financeanalyzer.dao.charts.model.YAxis; +import de.arminwolf.financeanalyzer.util.RandomColorUtil; + +import java.util.List; + +public class LineChartFactory { + + + public static HighChartDAO createLineChart(final String pTitle, final String pChartTitle, final List categories, final List pData) { + HighChartDAO lineChart = new HighChartDAO(); + Title title = new Title(); + title.setText(pTitle); + lineChart.setTitle(title); + + XAxis xAxis = new XAxis(); + xAxis.setCategories(categories); + + YAxis yAxis = new YAxis(); + Title chartTitle = new Title(); + chartTitle.setText(pChartTitle); + yAxis.setTitle(chartTitle); + + lineChart.setXAxis(xAxis); + lineChart.setYAxis(yAxis); + + + Chart chart = new Chart(); + chart.setType("line"); + + lineChart.setChart(chart); + lineChart.setColors(List.of(RandomColorUtil.getRandomColor())); + lineChart.setSeries(pData); + return lineChart; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Chart.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Chart.java new file mode 100644 index 0000000..6b3d837 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Chart.java @@ -0,0 +1,46 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import java.io.Serializable; + +public class Chart implements Serializable { + + private String type; + private String height; + + + public Chart() { + this.type = ""; + } + + + public Chart(final String type) { + this.type = type; + } + + + public Chart(final String type, final String height) { + this.type = type; + this.height = height; + } + + + public String getHeight() { + return height; + } + + + public void setHeight(final String height) { + this.height = height; + } + + + public String getType() { + return this.type; + } + + + public void setType(String type) { + this.type = type; + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Column.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Column.java new file mode 100644 index 0000000..19f8abb --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Column.java @@ -0,0 +1,20 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import java.io.Serializable; + +public class Column implements Serializable { + + private String stacking; + + + public String getStacking() { + return this.stacking; + } + + + public void setStacking(String stacking) { + this.stacking = stacking; + } + + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/DataLabels.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/DataLabels.java new file mode 100644 index 0000000..46e308c --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/DataLabels.java @@ -0,0 +1,59 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + + + +public class DataLabels { + + private boolean enabled; + private String format; + private Filter filter; + private Style style; + + + public DataLabels(boolean enabled, String format, Filter filter, Style style) { + this.enabled = enabled; + this.format = format; + this.filter = filter; + this.style = style; + } + + + public boolean isEnabled() { + return enabled; + } + + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + + public String getFormat() { + return format; + } + + + public void setFormat(final String format) { + this.format = format; + } + + + public Filter getFilter() { + return filter; + } + + + public void setFilter(final Filter filter) { + this.filter = filter; + } + + + public Style getStyle() { + return style; + } + + + public void setStyle(final Style style) { + this.style = style; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Filter.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Filter.java new file mode 100644 index 0000000..c75747d --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Filter.java @@ -0,0 +1,45 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +public class Filter { + + private String property; + private String operator; + private int value; + + + public Filter(String property, String operator, int value) { + this.property = property; + this.operator = operator; + this.value = value; + } + + + public String getProperty() { + return property; + } + + + public void setProperty(final String property) { + this.property = property; + } + + + public String getOperator() { + return operator; + } + + + public void setOperator(final String operator) { + this.operator = operator; + } + + + public int getValue() { + return value; + } + + + public void setValue(final int value) { + this.value = value; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/HighChartDAO.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/HighChartDAO.java new file mode 100644 index 0000000..c911652 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/HighChartDAO.java @@ -0,0 +1,125 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.List; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class HighChartDAO implements Serializable { + + private PlotOptions plotOptions; + private ToolTip tooltip; + private YAxis yAxis; + private XAxis xAxis; + private List series; + private Title title; + private Chart chart; + private List colors; + + + @JsonProperty("tooltip") + public ToolTip getTooltip() { + return tooltip; + } + + + public void setTooltip(final ToolTip tooltip) { + this.tooltip = tooltip; + } + + + public YAxis getyAxis() { + return yAxis; + } + + + public void setyAxis(final YAxis yAxis) { + this.yAxis = yAxis; + } + + + public XAxis getxAxis() { + return xAxis; + } + + + public void setxAxis(final XAxis xAxis) { + this.xAxis = xAxis; + } + + + public List getColors() { + return colors; + } + + + public void setColors(final List colors) { + this.colors = colors; + } + + + public PlotOptions getPlotOptions() { + return this.plotOptions; + } + + + public void setPlotOptions(PlotOptions plotOptions) { + this.plotOptions = plotOptions; + } + + + @JsonProperty("yAxis") + public YAxis getYAxis() { + return this.yAxis; + } + + + public void setYAxis(YAxis yAxis) { + this.yAxis = yAxis; + } + + + @JsonProperty("xAxis") + public XAxis getXAxis() { + return this.xAxis; + } + + + public void setXAxis(XAxis xAxis) { + this.xAxis = xAxis; + } + + + public List getSeries() { + return this.series; + } + + + public void setSeries(List series) { + this.series = series; + } + + + public Title getTitle() { + return this.title; + } + + + public void setTitle(Title title) { + this.title = title; + } + + + public Chart getChart() { + return this.chart; + } + + + public void setChart(Chart chart) { + this.chart = chart; + } + +} + diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/LayoutAlgorithm.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/LayoutAlgorithm.java new file mode 100644 index 0000000..2662df4 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/LayoutAlgorithm.java @@ -0,0 +1,76 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +public class LayoutAlgorithm { + + private double gravitationalConstant; + private boolean splitSeries; + private boolean seriesInteraction; + private boolean dragBetweenSeries; + private boolean parentNodeLimit; + + + public LayoutAlgorithm( + final double gravitationalConstant, + final boolean splitSeries, + final boolean seriesInteraction, + final boolean dragBetweenSeries, + final boolean parentNodeLimit + ) { + + this.gravitationalConstant = gravitationalConstant; + this.splitSeries = splitSeries; + this.seriesInteraction = seriesInteraction; + this.dragBetweenSeries = dragBetweenSeries; + this.parentNodeLimit = parentNodeLimit; + } + + + public double getGravitationalConstant() { + return gravitationalConstant; + } + + + public void setGravitationalConstant(final double gravitationalConstant) { + this.gravitationalConstant = gravitationalConstant; + } + + + public boolean isSplitSeries() { + return splitSeries; + } + + + public void setSplitSeries(final boolean splitSeries) { + this.splitSeries = splitSeries; + } + + + public boolean isSeriesInteraction() { + return seriesInteraction; + } + + + public void setSeriesInteraction(final boolean seriesInteraction) { + this.seriesInteraction = seriesInteraction; + } + + + public boolean isDragBetweenSeries() { + return dragBetweenSeries; + } + + + public void setDragBetweenSeries(final boolean dragBetweenSeries) { + this.dragBetweenSeries = dragBetweenSeries; + } + + + public boolean isParentNodeLimit() { + return parentNodeLimit; + } + + + public void setParentNodeLimit(final boolean parentNodeLimit) { + this.parentNodeLimit = parentNodeLimit; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Packedbubble.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Packedbubble.java new file mode 100644 index 0000000..32aefb8 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Packedbubble.java @@ -0,0 +1,95 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +public class Packedbubble { + + private String minSize; + private String maxSize; + private int zMin; + private int zMax; + private LayoutAlgorithm layoutAlgorithm; + private DataLabels dataLabels; + + + public Packedbubble(String minSize, String maxSize, int zMin, int zMax, LayoutAlgorithm layoutAlgorithm, DataLabels dataLabels) { + this.minSize = minSize; + this.maxSize = maxSize; + this.zMin = zMin; + this.zMax = zMax; + this.layoutAlgorithm = layoutAlgorithm; + this.dataLabels = dataLabels; + } + + + public Packedbubble() { + this.minSize = "20%"; + this.maxSize = "100%"; + this.zMin = 0; + this.zMax = 1000; + this.layoutAlgorithm = new LayoutAlgorithm(0.02, true, false, true, true); + this.dataLabels = new DataLabels( + true, "{point.name}", + new Filter("y", ">", 0), + new Style("black", "none", "normal") + ); + } + + + public String getMinSize() { + return minSize; + } + + + public void setMinSize(final String minSize) { + this.minSize = minSize; + } + + + public String getMaxSize() { + return maxSize; + } + + + public void setMaxSize(final String maxSize) { + this.maxSize = maxSize; + } + + + public int getzMin() { + return zMin; + } + + + public void setzMin(final int zMin) { + this.zMin = zMin; + } + + + public int getzMax() { + return zMax; + } + + + public void setzMax(final int zMax) { + this.zMax = zMax; + } + + + public LayoutAlgorithm getLayoutAlgorithm() { + return layoutAlgorithm; + } + + + public void setLayoutAlgorithm(final LayoutAlgorithm layoutAlgorithm) { + this.layoutAlgorithm = layoutAlgorithm; + } + + + public DataLabels getDataLabels() { + return dataLabels; + } + + + public void setDataLabels(final DataLabels dataLabels) { + this.dataLabels = dataLabels; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/PlotOptions.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/PlotOptions.java new file mode 100644 index 0000000..c715be5 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/PlotOptions.java @@ -0,0 +1,28 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +public class PlotOptions { + + private Packedbubble packedbubble; + + private Column column; + + + public Column getColumn() { + return this.column; + } + + + public void setColumn(Column column) { + this.column = column; + } + + + public Packedbubble getPackedbubble() { + return packedbubble; + } + + + public void setPackedbubble(final Packedbubble packedbubble) { + this.packedbubble = packedbubble; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Series.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Series.java new file mode 100644 index 0000000..c8e2776 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Series.java @@ -0,0 +1,62 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public class Series implements Serializable { + + private Set complexData; + private List data; + + private int stack = 0; + + private String name; + + + + public void setComplexData(Collection data) { + this.complexData = new HashSet<>(data); + } + + + public String getName() { + return this.name; + } + + + public int getStack() { + return stack; + } + + + public void setName(String name) { + this.name = name; + } + + + public void setData(final List data) { + this.data = data; + } + + + @JsonProperty("data") + public List getData() { + + if (Objects.isNull(data)) { + return this.complexData.stream().toList(); + } else { + return this.data; + } + } + + + public void setStack(final int i) { + this.stack = i; + } +} \ No newline at end of file diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/SeriesElement.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/SeriesElement.java new file mode 100644 index 0000000..e85f082 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/SeriesElement.java @@ -0,0 +1,50 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import java.util.Objects; + +public class SeriesElement { + + private String name; + private Float value; + + + public SeriesElement(final String name, final Float value) { + this.name = name; + this.value = value; + } + + + public String getName() { + return name; + } + + + public void setName(final String name) { + this.name = name; + } + + + public Float getValue() { + return value; + } + + + public void setValue(final Float value) { + this.value = value; + } + + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SeriesElement that = (SeriesElement) o; + return Objects.equals(name, that.name) && Objects.equals(value, that.value); + } + + + @Override + public int hashCode() { + return Objects.hash(name, value); + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Style.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Style.java new file mode 100644 index 0000000..facaaa8 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Style.java @@ -0,0 +1,46 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +public class Style { + + private String color; + private String textOutline; + private String fontWeight; + + + public Style(String color, String textOutline, String fontWeight) { + this.color = color; + this.textOutline = textOutline; + this.fontWeight = fontWeight; + } + + + public String getColor() { + return color; + } + + + public void setColor(final String color) { + this.color = color; + } + + + public String getTextOutline() { + return textOutline; + } + + + public void setTextOutline(final String textOutline) { + this.textOutline = textOutline; + } + + + public String getFontWeight() { + return fontWeight; + } + + + public void setFontWeight(final String fontWeight) { + this.fontWeight = fontWeight; + } +} + diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Title.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Title.java new file mode 100644 index 0000000..23a37b2 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/Title.java @@ -0,0 +1,28 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import java.io.Serializable; + +public class Title implements Serializable { + + private String text; + + + public Title() { + this.text = ""; + } + + + public Title(final String text) { + this.text = text; + } + + + public String getText() { + return this.text; + } + + + public void setText(String text) { + this.text = text; + } +} \ No newline at end of file diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/ToolTip.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/ToolTip.java new file mode 100644 index 0000000..d724710 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/ToolTip.java @@ -0,0 +1,39 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +public class ToolTip { + + private String pointFormat; + private boolean useHTML; + + + public ToolTip(String pointFormat, boolean useHTML) { + this.pointFormat = pointFormat; + this.useHTML = useHTML; + } + + + public ToolTip() { + this.pointFormat = "{point.name}: {point.value}"; + this.useHTML = true; + } + + + public String getPointFormat() { + return pointFormat; + } + + + public void setPointFormat(final String pointFormat) { + this.pointFormat = pointFormat; + } + + + public boolean isUseHTML() { + return useHTML; + } + + + public void setUseHTML(final boolean useHTML) { + this.useHTML = useHTML; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/XAxis.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/XAxis.java new file mode 100644 index 0000000..37f2461 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/XAxis.java @@ -0,0 +1,20 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import java.io.Serializable; +import java.util.Collection; + +public class XAxis implements Serializable { + + private Collection categories; + + + public Collection getCategories() { + return this.categories; + } + + + public void setCategories(Collection categories) { + this.categories = categories; + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/YAxis.java b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/YAxis.java new file mode 100644 index 0000000..071b33c --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/charts/model/YAxis.java @@ -0,0 +1,32 @@ +package de.arminwolf.financeanalyzer.dao.charts.model; + +import java.io.Serializable; + +public class YAxis implements Serializable { + + private Title title; + private int min; + + + public Title getTitle() { + return this.title; + } + + + public void setMin(final int i) { + this.min=i; + } + + + public int getMin() { + return min; + } + + + public void setTitle(Title title) { + this.title = title; + } + + + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/dao/model/OutputData.java b/src/main/java/de/arminwolf/financeanalyzer/dao/model/OutputData.java new file mode 100644 index 0000000..8f4c6df --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/dao/model/OutputData.java @@ -0,0 +1,147 @@ +package de.arminwolf.financeanalyzer.dao.model; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import de.arminwolf.financeanalyzer.util.DateUtil; +import de.arminwolf.financeanalyzer.util.NumberUtil; + +import java.time.LocalDate; +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +public class OutputData implements Comparable { + + @JsonProperty("id") + public String id; + @JsonProperty("amount") + public String amount; + @JsonProperty("amountAsFloat") + public Float amountAsFloat; + @JsonProperty("contractTurnaround") + public String contractTurnaround; + + @JsonProperty("contractTurnaround") + public String date; + @JsonProperty("recipient") + public String recipient; + @JsonProperty("purpose") + public String purpose; + + + + public OutputData(final Map.Entry order) { + this(order.getKey(), order.getValue()); + } + + + public OutputData(final String id, final TransactionDAO transaction) { + this.id = id; + this.amount = transaction.getBetrag().concat(" ").concat(transaction.getWaehrung()); + this.contractTurnaround = transaction.getAnalyseVertragsturnus().isBlank() ? "monatlich" : transaction.getAnalyseVertragsturnus(); + this.amountAsFloat = Float.parseFloat(transaction.getBetrag()); + this.date = DateUtil.prettyPrint(DateUtil.parse(transaction.getBuchungstag())); + this.purpose = transaction.getVerwendungszweck().trim().isEmpty() ? + "Überweisung an " + transaction.getBeguenstigterAuftraggeber() : + transaction.getVerwendungszweck(); + this.recipient = transaction.getBeguenstigterAuftraggeber(); + } + + + public OutputData(final TransactionDAO t) { + this(LocalDate.now().toString(), t); + } + + + public String getDate() { + return date; + } + + + public void setDate(final String date) { + this.date = date; + } + + + @Override + public int compareTo(final OutputData o) { + return getAmountAsFloat().compareTo(o.getAmountAsFloat()); + } + + + public Float getAmountAsFloat() { + return NumberUtil.round(amountAsFloat); + } + + + public void setAmountAsFloat(final Float amountAsFloat) { + this.amountAsFloat = amountAsFloat; + } + + + public String getId() { + return id; + } + + + public void setId(final String id) { + this.id = id; + } + + + public String getAmount() { + return amount; + } + + + public void setAmount(final String amount) { + this.amount = amount; + } + + + + public String getContractTurnaround() { + return contractTurnaround; + } + + + public void setContractTurnaround(final String contractTurnaround) { + this.contractTurnaround = contractTurnaround; + } + + + public String getPurpose() { + return purpose; + } + + + public void setPurpose(final String purpose) { + this.purpose = purpose; + } + + + public String getRecipient() { + return recipient; + } + + + public void setRecipient(final String recipient) { + this.recipient = recipient; + } + + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final OutputData outputData = (OutputData) o; + return Objects.equals(amount, outputData.amount) && Objects.equals(contractTurnaround, outputData.contractTurnaround) && Objects.equals(date, outputData.date) && Objects.equals(recipient, outputData.recipient) && Objects.equals(purpose, outputData.purpose); + } + + + @Override + public int hashCode() { + return Objects.hash(amount, contractTurnaround, date, recipient, purpose); + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/CacheService.java b/src/main/java/de/arminwolf/financeanalyzer/service/CacheService.java new file mode 100644 index 0000000..278adc2 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/CacheService.java @@ -0,0 +1,24 @@ +package de.arminwolf.financeanalyzer.service; + +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class CacheService { + + private final Map cache = new HashMap<>(); + + + public void put(final String key, final Object value) { + System.out.printf("Added to Cache. Key: [%s], Value: [%s]\n", key, value); + cache.put(key, value); + } + + + public Object get(final String key) { + return cache.get(key); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/ConverterService.java b/src/main/java/de/arminwolf/financeanalyzer/service/ConverterService.java new file mode 100644 index 0000000..ee38e5b --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/ConverterService.java @@ -0,0 +1,16 @@ +package de.arminwolf.financeanalyzer.service; + +import de.arminwolf.financeanalyzer.util.XlsxToJsonConverter; +import org.springframework.stereotype.Service; + +import java.io.InputStream; +import java.util.Optional; + +@Service +public class ConverterService { + + public Optional convert(InputStream in) { + return XlsxToJsonConverter.writeJson(XlsxToJsonConverter.readXlsx(in)); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/FileService.java b/src/main/java/de/arminwolf/financeanalyzer/service/FileService.java new file mode 100644 index 0000000..9ea9953 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/FileService.java @@ -0,0 +1,134 @@ +package de.arminwolf.financeanalyzer.service; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.arminwolf.financeanalyzer.conf.Configuration; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Stream; + +@Service +public class FileService { + + + public static final String CONFIGURATION_JSON = "configuration.json"; + @Autowired + private CacheService cacheService; + + + public Configuration getConfiguration() { + File workingDirectory = getOrCreateWorkingDirectory(); + Path configurationFilePath = Paths.get(workingDirectory.toPath().toString(), CONFIGURATION_JSON); + + System.out.printf("Loading '%s'\n", configurationFilePath); + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(configurationFilePath.toFile(), Configuration.class); + } catch (IOException e) { + System.out.printf("Error while accesing '%s'\n", configurationFilePath); + } + + return new Configuration(); + } + + + public Optional getJsonFileName() { + File workingDirectory = getOrCreateWorkingDirectory(); + final Path jsonFilePath = Paths.get(workingDirectory.toPath().toString(), "finance-analyzer.json"); + if (!jsonFilePath.toFile().exists()) { + return Optional.empty(); + } else { + String absolutePath = jsonFilePath.toAbsolutePath().toString(); + System.out.println("Absolute Path of json file: " + absolutePath); + return Optional.of(absolutePath); + } + } + + + public boolean isJsonPresent() { + File orCreateWorkingDirectory = getOrCreateWorkingDirectory(); + return getJsonFiles(orCreateWorkingDirectory).findFirst().isPresent(); + } + + + private Stream getJsonFiles(final File orCreateWorkingDirectory) { + return Arrays.stream(Objects.requireNonNull(orCreateWorkingDirectory.list((dir, name) -> name.endsWith(".json")))); + } + + + public TransactionDAO[] getJsonFile() { + File workingDirectory = getOrCreateWorkingDirectory(); + final Path jsonFilePath = Paths.get(workingDirectory.toPath().toString(), "finance-analyzer.json"); + if (!jsonFilePath.toFile().exists()) { + throw new IllegalStateException("Could not load json file."); + } else { + + System.out.printf("Loading '%s'\n", jsonFilePath); + ObjectMapper objectMapper = new ObjectMapper(); + try { + return objectMapper.readValue(jsonFilePath.toFile(), new TypeReference() { + }); + } catch (IOException e) { + System.out.printf("Error while accesing '%s'\n", jsonFilePath); + } + } + + return new TransactionDAO[0]; + } + + + public File getOrCreateWorkingDirectory() { + if (Objects.isNull(cacheService.get("workingDirectory"))) { + final String userHomeAsString = System.getProperty("user.home"); + System.out.printf("Looking for working Directory '.finance-analyser' in user home (%s).\n", userHomeAsString); + Path path = Paths.get(userHomeAsString.concat(File.separator).concat(".finance-analyser")); + boolean exists = Files.exists(path); + if (exists) { + return path.toFile(); + } else { + Path directory = null; + try { + directory = Files.createDirectory(path); + cacheService.put("workingDirectory", directory.toFile()); + return directory.toFile(); + } catch (IOException e) { + System.err.println("Could not create directory in user home.. Permission issue?" + e.getMessage()); + } + throw new IllegalStateException("Could not create working directory. Please fix the permissions."); + } + } else { + return (File) cacheService.get("workingDirectory"); + } + } + + + public void saveConfiguration(final Configuration configuration) { + File workingDirectory = getOrCreateWorkingDirectory(); + Path path = Paths.get(workingDirectory.getPath(), CONFIGURATION_JSON); + if (path.toFile().exists()) { + System.out.println("Configuration file already exists. Overwriting it."); + try { + Files.delete(path); + } catch (IOException e) { + System.out.println("Could not delete existing configuration file." + e.getMessage()); + } + } + ObjectMapper objectMapper = new ObjectMapper(); + try { + objectMapper.writeValue(path.toFile(), configuration); + } catch (IOException e) { + System.err.println("Could not save configuration file." + e.getMessage()); + } + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/ReportService.java b/src/main/java/de/arminwolf/financeanalyzer/service/ReportService.java new file mode 100644 index 0000000..37b426a --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/ReportService.java @@ -0,0 +1,181 @@ +package de.arminwolf.financeanalyzer.service; + +import de.arminwolf.financeanalyzer.conf.Configuration; +import de.arminwolf.financeanalyzer.dao.ReportDAO; +import de.arminwolf.financeanalyzer.dao.TableDataDAO; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import de.arminwolf.financeanalyzer.service.usecases.CashFlowService; +import de.arminwolf.financeanalyzer.service.usecases.IncomeService; +import de.arminwolf.financeanalyzer.service.usecases.OtherTransactionService; +import de.arminwolf.financeanalyzer.service.usecases.StandingOrderService; +import de.arminwolf.financeanalyzer.util.DateUtil; +import de.arminwolf.financeanalyzer.util.TransactionDateStringComparator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.ui.Model; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static de.arminwolf.financeanalyzer.util.Constants.AUSGABEN; +import static de.arminwolf.financeanalyzer.util.Constants.BEGUENSTIGTER; +import static de.arminwolf.financeanalyzer.util.Constants.BETRAG; +import static de.arminwolf.financeanalyzer.util.Constants.DATUM; +import static de.arminwolf.financeanalyzer.util.Constants.EINNAHMEN; +import static de.arminwolf.financeanalyzer.util.Constants.TURNUS; +import static de.arminwolf.financeanalyzer.util.Constants.VERWENDUNGSZWECK; + +@Service +public class ReportService { + + + public static final String EUR = " EUR"; + @Autowired + private IncomeService incomeService; + @Autowired + private CashFlowService cashFlowService; + + @Autowired + private StandingOrderService standingOrderService; + + @Autowired + private OtherTransactionService otherTransactionService; + + @Autowired + private CacheService cacheService; + + private ReportDAO report; + + + public ReportDAO createReport(final TransactionDAO[] response, String month, String iban, final Model model) { + this.report = new ReportDAO(); + System.out.println("LocalDate : " + DateUtil.parseMonth(month)); + int numberOfMonths = DateUtil.monthsBetween(DateUtil.parseMonth(month), LocalDate.now()); + final List filteredIncomeTransactions = getFilteredIncomeTransactions(response, numberOfMonths, iban); + final List filteredExpenseTransactions = getFilteredExpenseTransactions(response, numberOfMonths, iban); + + // Verarbeiten Sie hier die Antwort + ((Configuration) cacheService.get("configuration")).getBankAccounts().stream() + .filter(bankAccount -> bankAccount.getIban().equals(iban)).findFirst() + .ifPresent(bankAccount -> { + model.addAttribute("accountName", bankAccount.getBankAccountName()); + model.addAttribute("iban", iban); + }); + + // Daueraufträge + standingOrderService.setStandingOrder(report, filteredExpenseTransactions); + + // Sonstige Transaktionen + otherTransactionService.setOtherTransaction(report, filteredExpenseTransactions); + + // Cashflow + cashFlowService.setCashFlowChart(report, filteredIncomeTransactions, filteredExpenseTransactions); + + // Einkommen + incomeService.setIncome(report, filteredIncomeTransactions); + + + LocalDate localDate = LocalDate.now().minusMonths(numberOfMonths); + final LocalDate start = DateUtil.getFirstDayOfMonth(localDate); + final LocalDate end = DateUtil.getLastDayOfMonth(localDate); + + model.addAttribute("date", String.format("%s - %s", DateUtil.format(start), DateUtil.format(end))); + model.addAttribute("numberOfMonths", numberOfMonths); + + + model.addAttribute("cashFlow", getCashFlowData()); + model.addAttribute("income", getIncomeData()); + model.addAttribute("standingOrders", getStandingOrdersData()); + model.addAttribute("notStandingOrders", getNotStandingOrdersData()); + + model.addAttribute("incomeExpenseColumnReport", report.getCashFlowChart()); + model.addAttribute("accountBalanceLineChart", report.getAccountBalanceLineChart()); + model.addAttribute("transactionCategories", report.getTransactionCategoriesBubbleChart()); + + return report; + } + + + private TableDataDAO getCashFlowData() { + TableDataDAO cashFlowData = TableDataDAO.TableDataFactory.createTableDataDTO(); + cashFlowData.setHeaders(Arrays.asList("Einnahmen", "Verträge", "Ausgaben (ohne Veträge)", "Ausgaben insgesammt", "Differenz")); + cashFlowData.setRows(List.of( + Arrays.asList( + String.valueOf(report.getIncome()).concat(EUR), + String.valueOf(report.getStandingOrdersAmount()).concat(EUR), + String.valueOf(report.getNotStandingOrdersAmount()).concat(EUR), + String.valueOf(report.getTotalExpense()).concat(EUR), + String.valueOf(report.getDiff()).concat(EUR))) + ); + return cashFlowData; + } + + + private List getFilteredIncomeTransactions(final TransactionDAO[] response, final int numberOfMonths, final String iban) { + return getFilteredTransactions(response, EINNAHMEN, numberOfMonths, iban); + } + + + private List getFilteredExpenseTransactions(final TransactionDAO[] response, final int numberOfMonths, final String iban) { + return getFilteredTransactions(response, AUSGABEN, numberOfMonths, iban); + } + + + private List getFilteredTransactions(final TransactionDAO[] response, final String type, int numberOfMonths, String iban) { + final LocalDate localDate = LocalDate.now().minusMonths(numberOfMonths); + final LocalDate start = LocalDate.of(localDate.getYear(), localDate.getMonth(), 1); + final LocalDate end = YearMonth.of(localDate.getYear(), localDate.getMonth()).atEndOfMonth(); + + return Arrays.stream(response) + .filter(t -> DateUtil.isBetween(t.getBuchungstag(), start, end)) + .filter(t -> t.getReferenzkonto().equals(iban)) + .filter(e -> e.getAnalyseBetrag().equals(type)) + .sorted(new TransactionDateStringComparator()) + .collect(Collectors.toList()); + } + + + + private TableDataDAO getIncomeData() { + TableDataDAO incomeData = TableDataDAO.TableDataFactory.createTableDataDTO(); + incomeData.setHeaders(Arrays.asList(BETRAG, DATUM, BEGUENSTIGTER, VERWENDUNGSZWECK)); + incomeData.setRows(report.getIncomeList().stream() + .map(income -> Arrays.asList( + income.getAmount(), + income.getDate(), + income.getRecipient(), + income.getPurpose())).toList()); + return incomeData; + } + + + private TableDataDAO getNotStandingOrdersData() { + TableDataDAO notStandingOrders = TableDataDAO.TableDataFactory.createTableDataDTO(); + notStandingOrders.setHeaders(Arrays.asList(BETRAG, DATUM, TURNUS, BEGUENSTIGTER, VERWENDUNGSZWECK)); + notStandingOrders.setRows(report.getSortedNotStandingOrders().stream() + .map(income -> Arrays.asList( + income.getAmount(), + income.getDate(), + income.getContractTurnaround(), + income.getRecipient(), + income.getPurpose())).toList()); + return notStandingOrders; + } + + + private TableDataDAO getStandingOrdersData() { + TableDataDAO standingOrders = TableDataDAO.TableDataFactory.createTableDataDTO(); + standingOrders.setHeaders(Arrays.asList(BETRAG, DATUM, TURNUS, BEGUENSTIGTER, VERWENDUNGSZWECK)); + standingOrders.setRows(report.getSortedStandingOrders().stream() + .map(income -> Arrays.asList( + income.getAmount(), + income.getDate(), + income.getContractTurnaround(), + income.getRecipient(), + income.getPurpose())).toList()); + return standingOrders; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/usecases/CashFlowService.java b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/CashFlowService.java new file mode 100644 index 0000000..b713946 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/CashFlowService.java @@ -0,0 +1,67 @@ +package de.arminwolf.financeanalyzer.service.usecases; + +import de.arminwolf.financeanalyzer.dao.ReportDAO; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import org.apache.commons.math3.util.Pair; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import static de.arminwolf.financeanalyzer.util.Constants.AUSGABEN; +import static de.arminwolf.financeanalyzer.util.Constants.EINNAHMEN; + +@Service +public class CashFlowService { + + public void setCashFlowChart(final ReportDAO reportDAO, + final List filteredIncomeTransactions, + final List filteredExpenseTransactions) { + Map> expenses = new TreeMap<>(); + Map> income = new TreeMap<>(); + + filteredIncomeTransactions.stream() + .map(TransactionDAO::getAnalyseMonat) + .forEach(month -> { + income.put(month, new ArrayList<>()); + filteredIncomeTransactions.stream() + .filter(t -> t.getAnalyseMonat().equals(month)) + .filter(t -> t.getAnalyseBetrag().equals(EINNAHMEN)) + .forEach(t -> income.get(month).add(t.getBetragAsFloat())); + + }); + + filteredExpenseTransactions.stream() + .map(TransactionDAO::getAnalyseMonat) + .forEach(month -> { + expenses.put(month, new ArrayList<>()); + filteredExpenseTransactions.stream() + .filter(t -> t.getAnalyseMonat().equals(month)) + .filter(t -> t.getAnalyseBetrag().equals(AUSGABEN)) + .forEach(t -> expenses.get(month).add(t.getBetragAsFloat())); + }); + + + + System.out.println("Number of Dataseries. Expenses: " + expenses.size() + ", Income: " + income.size()); + income.values().forEach(System.out::println); + + System.out.println("-------------------"); + reportDAO.setCashFlowChart(expenses, income); + + final List allTransaction = new ArrayList<>(); + allTransaction.addAll(filteredIncomeTransactions); + allTransaction.addAll(filteredExpenseTransactions); + + // Line Chart + reportDAO.setAccountBalanceLineChart(allTransaction.stream() + .map(t -> Pair.create(t.getBuchungstag(), t.getKontostand())) + .collect(Collectors.toList())); + + // Bubble Chart + reportDAO.setTransactionCategoriesBubbleChart(filteredExpenseTransactions); + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/usecases/IncomeService.java b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/IncomeService.java new file mode 100644 index 0000000..58af6b3 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/IncomeService.java @@ -0,0 +1,21 @@ +package de.arminwolf.financeanalyzer.service.usecases; + +import de.arminwolf.financeanalyzer.dao.ReportDAO; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +import static de.arminwolf.financeanalyzer.util.Constants.EINNAHMEN; + +@Service +public class IncomeService { + + public void setIncome(final ReportDAO reportDAO, final List filteredTransactions) { + reportDAO.setIncomeList(filteredTransactions.stream() + .filter(e -> e.getAnalyseBetrag().equals(EINNAHMEN)) + .collect(Collectors.toList())); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/usecases/OtherTransactionService.java b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/OtherTransactionService.java new file mode 100644 index 0000000..69c4461 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/OtherTransactionService.java @@ -0,0 +1,23 @@ +package de.arminwolf.financeanalyzer.service.usecases; + +import de.arminwolf.financeanalyzer.dao.ReportDAO; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import de.arminwolf.financeanalyzer.dao.model.OutputData; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class OtherTransactionService { + + public void setOtherTransaction(final ReportDAO reportDAO, + final List filteredTransactions) { + + reportDAO.setNotStandingOrders(filteredTransactions.stream() + .filter(t -> !reportDAO.getStandingOrders().contains(new OutputData(t))) + .distinct() + .collect(Collectors.toList())); + + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/service/usecases/StandingOrderService.java b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/StandingOrderService.java new file mode 100644 index 0000000..6f2c5e3 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/service/usecases/StandingOrderService.java @@ -0,0 +1,29 @@ +package de.arminwolf.financeanalyzer.service.usecases; + +import de.arminwolf.financeanalyzer.dao.ReportDAO; +import de.arminwolf.financeanalyzer.dao.TransactionDAO; +import de.arminwolf.financeanalyzer.util.StreamUtil; +import de.arminwolf.financeanalyzer.util.TransactionDateStringReverseComparator; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class StandingOrderService { + + public static final String JA = "ja"; + + + public void setStandingOrder(final ReportDAO reportDAO, final List filteredTransactions) { + + List transactions = filteredTransactions.stream() + .filter(e -> e.getAnalyseVertrag().equals(JA) + || e.getAnalyseUmsatzart().equals("Dauerauftrag") + || e.getAnalyseUmsatzart().equals("Lastschrift")) + .sorted(new TransactionDateStringReverseComparator()) + .toList(); + + List standingOrderTransactions = StreamUtil.findDuplicateInStream(transactions); + reportDAO.setStandingOrders(standingOrderTransactions); + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/Constants.java b/src/main/java/de/arminwolf/financeanalyzer/util/Constants.java new file mode 100644 index 0000000..a878a4e --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/Constants.java @@ -0,0 +1,18 @@ +package de.arminwolf.financeanalyzer.util; + +public class Constants { + + public static final String INDEX = "upload"; + public static final String ERROR = "templates/error"; + public static final String REPORTS = "reports"; + + public static final String EINNAHMEN = "Einnahmen"; + public static final String AUSGABEN = "Ausgaben"; + public static final String GEHALT = "Gehalt"; + + public static final String TURNUS = "Turnus"; + public static final String VERWENDUNGSZWECK = "Verwendungszweck"; + public static final String BEGUENSTIGTER = "Begünstigter"; + public static final String DATUM = "Datum"; + public static final String BETRAG = "Betrag"; +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/DateFormatStringComparatorForOrder.java b/src/main/java/de/arminwolf/financeanalyzer/util/DateFormatStringComparatorForOrder.java new file mode 100644 index 0000000..61b4529 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/DateFormatStringComparatorForOrder.java @@ -0,0 +1,9 @@ +package de.arminwolf.financeanalyzer.util; + +import de.arminwolf.financeanalyzer.dao.model.OutputData; + +import java.util.Comparator; + +public interface DateFormatStringComparatorForOrder extends Comparator { + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/DateUtil.java b/src/main/java/de/arminwolf/financeanalyzer/util/DateUtil.java new file mode 100644 index 0000000..f2275f7 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/DateUtil.java @@ -0,0 +1,113 @@ +package de.arminwolf.financeanalyzer.util; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; + +public class DateUtil { + + private final static DateFormat df = new SimpleDateFormat("dd-MMM-yyyy", Locale.GERMAN); + private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); + private final static DateTimeFormatter originalFormatter = DateTimeFormatter.ofPattern("dd-MMM-yyyy"); + private final static Calendar startCalendar = Calendar.getInstance(); + + + public static String format(final LocalDate start) { + return start.format(formatter); + } + + + public static LocalDate getFirstDayOfMonth(final LocalDate localDate) { + return LocalDate.of(localDate.getYear(), localDate.getMonth(), 1); + } + + + public static LocalDate getLastDayOfMonth(final LocalDate localDate) { + return YearMonth.of(localDate.getYear(), localDate.getMonth()).atEndOfMonth(); + } + + + public static boolean isBefore(String date, LocalDate end) { + try { + Date parse = df.parse(date); + Instant instant = parse.toInstant(); + LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate(); + + return localDate.isBefore(end); + } catch (ParseException e) { + System.out.println("ERROR: " + date); + return false; + } + } + + + public static boolean isAfter(String date, LocalDate start) { + try { + Date parse = df.parse(date); + Instant instant = parse.toInstant(); + LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate(); + + return localDate.isAfter(start); + } catch (ParseException e) { + System.out.println("ERROR: " + date); + e.printStackTrace(System.err); + return false; + } + } + + + public static boolean isBetween(String date, LocalDate start, LocalDate end) { + try { + Date parse = df.parse(date); + Instant instant = parse.toInstant(); + LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate(); + + return (localDate.isEqual(end) || localDate.isBefore(end)) && (localDate.isEqual(start) || localDate.isAfter(start)); + } catch (ParseException e) { + System.out.println("ERROR: " + date); + e.printStackTrace(System.err); + return false; + } + } + + + public static LocalDate parse(final String buchungstag) { + return LocalDate.parse(buchungstag, originalFormatter); + } + + + public static LocalDate parseMonth(final String month) { + String[] split = month.split("-"); + return LocalDate.of(Integer.parseInt(split[0]), Integer.parseInt(split[1]), 1); + } + + + public static String prettyPrint(final LocalDate date) { + return date.format(formatter); + } + + public static int monthsBetween(LocalDate startDate, LocalDate endDate) { + if (startDate == null || endDate == null) { + throw new IllegalArgumentException("Both startDate and endDate must be provided"); + } + + startCalendar.setTime(Date.from(startDate.atStartOfDay(ZoneId.systemDefault()).toInstant())); + int startDateTotalMonths = 12 * startCalendar.get(Calendar.YEAR) + startCalendar.get(Calendar.MONTH); + + Calendar endCalendar = Calendar.getInstance(); + endCalendar.setTime(Date.from(endDate.atStartOfDay(ZoneId.systemDefault()).toInstant())); + + int endDateTotalMonths = 12 * endCalendar.get(Calendar.YEAR) + + endCalendar.get(Calendar.MONTH); + + return endDateTotalMonths - startDateTotalMonths; + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/NumberUtil.java b/src/main/java/de/arminwolf/financeanalyzer/util/NumberUtil.java new file mode 100644 index 0000000..faa7e7c --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/NumberUtil.java @@ -0,0 +1,16 @@ +package de.arminwolf.financeanalyzer.util; + +import java.util.Objects; + +public class NumberUtil { + + public static Float round(final Float value) { + return round(value, 2); + } + + + public static Float round(final Float value, final int precision) { + return Math.round((Objects.isNull(value) ? 0f : value) * Math.pow(10, precision)) / (float) Math.pow(10, precision); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/OrderDateStringComparator.java b/src/main/java/de/arminwolf/financeanalyzer/util/OrderDateStringComparator.java new file mode 100644 index 0000000..31877a7 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/OrderDateStringComparator.java @@ -0,0 +1,20 @@ +package de.arminwolf.financeanalyzer.util; + +import de.arminwolf.financeanalyzer.dao.model.OutputData; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Comparator; + +public class OrderDateStringComparator implements Comparator { + + @Override + public int compare(final OutputData o1, final OutputData o2) { + SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy"); + try { + return -1 * formatter.parse(o1.getDate()).compareTo(formatter.parse(o2.getDate())); + } catch (ParseException e) { + return -1; + } + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/RandomColorUtil.java b/src/main/java/de/arminwolf/financeanalyzer/util/RandomColorUtil.java new file mode 100644 index 0000000..15878f4 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/RandomColorUtil.java @@ -0,0 +1,17 @@ +package de.arminwolf.financeanalyzer.util; + +import java.util.Random; + +public class RandomColorUtil { + + private static final Random random = new Random(); + + + public static String getRandomColor() { + // create a big random number - maximum is ffffff (hex) = 16777215 (dez) + int nextInt = random.nextInt(0xffffff + 1); + + // format it as hexadecimal string (with hashtag and leading zeros) + return String.format("#%06x", nextInt); + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/StreamUtil.java b/src/main/java/de/arminwolf/financeanalyzer/util/StreamUtil.java new file mode 100644 index 0000000..bd3a43e --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/StreamUtil.java @@ -0,0 +1,29 @@ +package de.arminwolf.financeanalyzer.util; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +public class StreamUtil { + + public static List findDuplicateInStream(List list) { + + // Set to store the duplicate elements + Set items = new HashSet<>(); + + // Return the set of duplicate elements + return list.stream() + + // Set.add() returns false + // if the element was + // already present in the set. + // Hence filter such elements + .filter(items::add) + + // Collect duplicate elements + // in the set + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/TransactionDateStringComparator.java b/src/main/java/de/arminwolf/financeanalyzer/util/TransactionDateStringComparator.java new file mode 100644 index 0000000..d931166 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/TransactionDateStringComparator.java @@ -0,0 +1,22 @@ +package de.arminwolf.financeanalyzer.util; + +import de.arminwolf.financeanalyzer.dao.TransactionDAO; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Comparator; + +public class TransactionDateStringComparator implements Comparator { + + @Override + public int compare(final TransactionDAO o1, final TransactionDAO o2) { + SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy"); + try { + return formatter.parse(o1.getBuchungstag()).compareTo(formatter.parse(o2.getBuchungstag())); + } catch (ParseException e) { + return -1; + } + } + + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/TransactionDateStringReverseComparator.java b/src/main/java/de/arminwolf/financeanalyzer/util/TransactionDateStringReverseComparator.java new file mode 100644 index 0000000..59a770f --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/TransactionDateStringReverseComparator.java @@ -0,0 +1,22 @@ +package de.arminwolf.financeanalyzer.util; + +import de.arminwolf.financeanalyzer.dao.TransactionDAO; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Comparator; + +public class TransactionDateStringReverseComparator implements Comparator { + + @Override + public int compare(final TransactionDAO o1, final TransactionDAO o2) { + SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy"); + try { + return -1 * formatter.parse(o1.getBuchungstag()).compareTo(formatter.parse(o2.getBuchungstag())); + } catch (ParseException e) { + return -1; + } + } + + +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/UrlUtil.java b/src/main/java/de/arminwolf/financeanalyzer/util/UrlUtil.java new file mode 100644 index 0000000..7978b7c --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/UrlUtil.java @@ -0,0 +1,12 @@ +package de.arminwolf.financeanalyzer.util; + +import jakarta.servlet.http.HttpServletRequest; + +public class UrlUtil { + + public static String getRelativeUrl(final HttpServletRequest request, final String path) { + String requestURL = request.getRequestURL().toString(); + String servletPath = request.getServletPath(); + return requestURL.substring(0, requestURL.lastIndexOf(servletPath)).concat(path); + } +} diff --git a/src/main/java/de/arminwolf/financeanalyzer/util/XlsxToJsonConverter.java b/src/main/java/de/arminwolf/financeanalyzer/util/XlsxToJsonConverter.java new file mode 100644 index 0000000..e6df161 --- /dev/null +++ b/src/main/java/de/arminwolf/financeanalyzer/util/XlsxToJsonConverter.java @@ -0,0 +1,68 @@ +package de.arminwolf.financeanalyzer.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.poi.openxml4j.util.ZipSecureFile; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class XlsxToJsonConverter { + + + public static final String JSON_EXTENSION = ".json"; + + + public static List> readXlsx(InputStream inputFile) { + List> data = new ArrayList<>(); + ZipSecureFile.setMinInflateRatio(0.001); + try (XSSFWorkbook workbook = new XSSFWorkbook(inputFile)) { + Sheet sheet = workbook.getSheetAt(0); + Row headerRow = sheet.getRow(0); + int lastColumnNum = headerRow.getLastCellNum(); + for (int i = 1; i <= sheet.getLastRowNum(); i++) { + Row row = sheet.getRow(i); + Map rowData = new HashMap<>(); + for (int j = 0; j < lastColumnNum; j++) { + Cell cell = row.getCell(j); + String cellValue = ""; + if (Objects.nonNull(cell)) { + cellValue = cell.toString(); + } + rowData.put(convert2jsonKey(headerRow.getCell(j).toString()), cellValue); + } + data.add(rowData); + } + } catch (IOException e) { + e.printStackTrace(); + } + return data; + } + + + private static String convert2jsonKey(final String string) { + return string.toLowerCase(Locale.GERMANY).replaceAll(" ", "-"); + } + + + public static Optional writeJson(List> data) { + try (ByteArrayOutputStream fos = new ByteArrayOutputStream()) { + ObjectMapper mapper = new ObjectMapper(); + mapper.writerWithDefaultPrettyPrinter().writeValue(fos, data); + return Optional.of(fos.toString()); + } catch (Exception e) { + return Optional.empty(); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css new file mode 100644 index 0000000..54e7533 --- /dev/null +++ b/src/main/resources/static/css/styles.css @@ -0,0 +1,137 @@ +:root { + line-height: 1.6; + font-size: 0.9rem; +} + +body { + font-family: Courier, monospace; + width: auto; + color: #666; /* Helle Textfarbe */ + background-color: #fff; + margin: 0; + padding: 0; + transition: background-color 0.3s ease; /* Beispiel für Übergänge */ +} + +.card-header { + border-radius: calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0; + background-color: #343a40; +} + +.card-header p { + color: #333; +} + +/* Überschriften */ +h1, h2, h3, h4, h5, h6 { + font-weight: bold; + color: #fff; /* Helle Textfarbe für Links und Schaltflächen */ + font-family: Courier, monospace; +} + +a, .btn { + color: #fff; /* Helle Textfarbe für Links und Schaltflächen */ +} + +a:hover, .btn:hover { + background-color: #444; /* Dunklere Hintergrundfarbe beim Hovern über Links und Schaltflächen */ +} + + +/* Texte */ +p, span, label { + line-height: 1.25; + font-family: Courier, monospace; +} + +/* Links */ +a { + color: #007bff; +} + +.red { + color: red; +} + +.green { + color: green; +} + +/* Fehlermeldungen */ +.error { + color: #dc3545; + font-size: 14px; +} + +.content-section { + width: 85%; + margin: 0 auto; + padding: 5em; +} + +hr { + margin-top: 1rem; + margin-bottom: 1rem; + border: 0; + border-top: 1px solid rgba(0, 0, 0, 0.1); +} + +/* Container */ +.container { + margin: 2em auto; + padding: 2em; + background-color: #fff; + box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3); + font-family: Courier, monospace; +} + +.flex-container { + display: flex; + justify-content: space-between; +} + +ul { + list-style: none; + margin: 0; + padding: 0; + font-family: Courier, monospace; +} +.scrollable-table { + overflow: auto; /* Scrollen aktivieren, wenn der Inhalt überläuft */ +} + +table { + width: 100%; + border-collapse: collapse; + border-spacing: 0; +} +th, td { + padding: 5px; + text-align: right; +} +th { + background-color: #f2f2f2; + color: #333; + font-weight: bold; + text-align: right; +} + +tr:nth-child(even) { + background-color: #f9f9f9; +} + +tr:hover { + background-color: #f5f5f5; +} +/* Definiert die Trennlinie für die Zeile */ +tr.border-row td { + border-top: 1px solid darkgrey; +} + +[data-analysis-value^="-"] { + color: red; +} + +[data-analysis-value$="EUR"]:not([data-analysis-value^="-"]) { + color: green; +} \ No newline at end of file diff --git a/src/main/resources/templates/error/404.html b/src/main/resources/templates/error/404.html new file mode 100644 index 0000000..93834dd --- /dev/null +++ b/src/main/resources/templates/error/404.html @@ -0,0 +1,22 @@ + + + + + Error 404 - Page not found + + + + + + + +
+
+
+

Error - Page not found

+
+
+
+
+ + diff --git a/src/main/resources/templates/error/500.html b/src/main/resources/templates/error/500.html new file mode 100644 index 0000000..ebb9942 --- /dev/null +++ b/src/main/resources/templates/error/500.html @@ -0,0 +1,22 @@ + + + + + Error 404 - Page not found + + + + + + + +
+
+
+

Internal Server Error

+
+
+
+
+ + diff --git a/src/main/resources/templates/fragments/body_end_scripts.html b/src/main/resources/templates/fragments/body_end_scripts.html new file mode 100644 index 0000000..5c39f4c --- /dev/null +++ b/src/main/resources/templates/fragments/body_end_scripts.html @@ -0,0 +1,7 @@ +
+ +
diff --git a/src/main/resources/templates/fragments/html_head.html b/src/main/resources/templates/fragments/html_head.html new file mode 100644 index 0000000..8347444 --- /dev/null +++ b/src/main/resources/templates/fragments/html_head.html @@ -0,0 +1,19 @@ + + + + Dashboard + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/navigation.html b/src/main/resources/templates/fragments/navigation.html new file mode 100644 index 0000000..ee81ea6 --- /dev/null +++ b/src/main/resources/templates/fragments/navigation.html @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/page_title.html b/src/main/resources/templates/fragments/page_title.html new file mode 100644 index 0000000..31ce3a6 --- /dev/null +++ b/src/main/resources/templates/fragments/page_title.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/table.html b/src/main/resources/templates/fragments/table.html new file mode 100644 index 0000000..cde8b76 --- /dev/null +++ b/src/main/resources/templates/fragments/table.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +
Betrag
\ No newline at end of file diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html new file mode 100644 index 0000000..4e6cb0f --- /dev/null +++ b/src/main/resources/templates/layout.html @@ -0,0 +1,14 @@ + + + + + + + + +
+ + +
+ + diff --git a/src/main/resources/templates/reports.html b/src/main/resources/templates/reports.html new file mode 100644 index 0000000..4fb4b61 --- /dev/null +++ b/src/main/resources/templates/reports.html @@ -0,0 +1,119 @@ + + + +
+
+
+
+
+ Kontodaten +
+
+

+

+

+
+
+
+
+ +
+
+
+
+ Cashflow Analyse +
+
+
+
+
+
+
+ +
+ +
+
+
+ Kontostand Verlauf +
+
+
+
+
+
+ + +
+
+
+ Verteilung Einnahmen und Ausgaben +
+
+
+
+
+
+ , +
+
+
+
+
+ Einkommen +
+ +
+
+
+
+
+
+ +
+
+
+
Verträge
+
+
+
+
+
+
+
+
+
+
Keine Verträge
+
+
+
+
+
+
+ +
+ +
+
+
Kategorien der Ausgaben
+
+
+
+
+
+ +
+ + + +
+ +
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/upload.html b/src/main/resources/templates/upload.html new file mode 100644 index 0000000..3ffb385 --- /dev/null +++ b/src/main/resources/templates/upload.html @@ -0,0 +1,50 @@ + + + +
+
+
+
+
+

Bestehnde Daten gefunden:

+
+
+

+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+ Upload +
+
+
+ + +
+
+
+
+
+
+
\ No newline at end of file