Initial commit with basic working API
Signed-off-by: Billy Brawner <billy@wbrawner.com>
This commit is contained in:
commit
e2a35ec249
29 changed files with 1477 additions and 0 deletions
29
.gitignore
vendored
Normal file
29
.gitignore
vendored
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
HELP.md
|
||||||
|
/target/
|
||||||
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
|
|
||||||
|
### 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/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
114
.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
114
.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
public class MavenWrapperDownloader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_DOWNLOAD_URL =
|
||||||
|
"https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
|
||||||
|
* use instead of the default one.
|
||||||
|
*/
|
||||||
|
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
|
||||||
|
".mvn/wrapper/maven-wrapper.properties";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Path where the maven-wrapper.jar will be saved to.
|
||||||
|
*/
|
||||||
|
private static final String MAVEN_WRAPPER_JAR_PATH =
|
||||||
|
".mvn/wrapper/maven-wrapper.jar";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the property which should be used to override the default download url for the wrapper.
|
||||||
|
*/
|
||||||
|
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
|
||||||
|
|
||||||
|
public static void main(String args[]) {
|
||||||
|
System.out.println("- Downloader started");
|
||||||
|
File baseDirectory = new File(args[0]);
|
||||||
|
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
|
||||||
|
|
||||||
|
// If the maven-wrapper.properties exists, read it and check if it contains a custom
|
||||||
|
// wrapperUrl parameter.
|
||||||
|
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
|
||||||
|
String url = DEFAULT_DOWNLOAD_URL;
|
||||||
|
if (mavenWrapperPropertyFile.exists()) {
|
||||||
|
FileInputStream mavenWrapperPropertyFileInputStream = null;
|
||||||
|
try {
|
||||||
|
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
|
||||||
|
Properties mavenWrapperProperties = new Properties();
|
||||||
|
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
|
||||||
|
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
|
||||||
|
} catch (IOException e) {
|
||||||
|
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (mavenWrapperPropertyFileInputStream != null) {
|
||||||
|
mavenWrapperPropertyFileInputStream.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Ignore ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("- Downloading from: : " + url);
|
||||||
|
|
||||||
|
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
|
||||||
|
if (!outputFile.getParentFile().exists()) {
|
||||||
|
if (!outputFile.getParentFile().mkdirs()) {
|
||||||
|
System.out.println(
|
||||||
|
"- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
|
||||||
|
try {
|
||||||
|
downloadFileFromURL(url, outputFile);
|
||||||
|
System.out.println("Done");
|
||||||
|
System.exit(0);
|
||||||
|
} catch (Throwable e) {
|
||||||
|
System.out.println("- Error downloading");
|
||||||
|
e.printStackTrace();
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
|
||||||
|
URL website = new URL(urlString);
|
||||||
|
ReadableByteChannel rbc;
|
||||||
|
rbc = Channels.newChannel(website.openStream());
|
||||||
|
FileOutputStream fos = new FileOutputStream(destination);
|
||||||
|
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||||
|
fos.close();
|
||||||
|
rbc.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
1
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
1
.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip
|
286
mvnw
vendored
Executable file
286
mvnw
vendored
Executable file
|
@ -0,0 +1,286 @@
|
||||||
|
#!/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.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Maven2 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 /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)`"
|
||||||
|
# TODO classpath?
|
||||||
|
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="`which 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
|
||||||
|
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
|
||||||
|
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 command -v wget > /dev/null; then
|
||||||
|
if [ "$MVNW_VERBOSE" = true ]; then
|
||||||
|
echo "Found wget ... using wget"
|
||||||
|
fi
|
||||||
|
wget "$jarUrl" -O "$wrapperJarPath"
|
||||||
|
elif command -v curl > /dev/null; then
|
||||||
|
if [ "$MVNW_VERBOSE" = true ]; then
|
||||||
|
echo "Found curl ... using curl"
|
||||||
|
fi
|
||||||
|
curl -o "$wrapperJarPath" "$jarUrl"
|
||||||
|
else
|
||||||
|
if [ "$MVNW_VERBOSE" = true ]; then
|
||||||
|
echo "Falling back to using Java to download"
|
||||||
|
fi
|
||||||
|
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||||
|
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
|
||||||
|
|
||||||
|
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||||
|
|
||||||
|
exec "$JAVACMD" \
|
||||||
|
$MAVEN_OPTS \
|
||||||
|
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||||
|
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||||
|
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
161
mvnw.cmd
vendored
Normal file
161
mvnw.cmd
vendored
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
@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 Maven2 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 key stroke 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 my 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 "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
|
||||||
|
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\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/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
|
||||||
|
FOR /F "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% (
|
||||||
|
echo Found %WRAPPER_JAR%
|
||||||
|
) else (
|
||||||
|
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||||
|
echo Downloading from: %DOWNLOAD_URL%
|
||||||
|
powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
|
||||||
|
echo Finished downloading %WRAPPER_JAR%
|
||||||
|
)
|
||||||
|
@REM End of extension
|
||||||
|
|
||||||
|
%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 "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
|
||||||
|
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\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%
|
||||||
|
|
||||||
|
exit /B %ERROR_CODE%
|
113
pom.xml
Normal file
113
pom.xml
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>2.1.4.RELEASE</version>
|
||||||
|
<relativePath/> <!-- lookup parent from repository -->
|
||||||
|
</parent>
|
||||||
|
<groupId>com.wbrawner</groupId>
|
||||||
|
<artifactId>budget-server</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>budget-server</name>
|
||||||
|
<description>The server-side component for the budgeting apps</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<java.version>1.8</java.version>
|
||||||
|
<kotlin.version>1.2.71</kotlin.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.module</groupId>
|
||||||
|
<artifactId>jackson-module-kotlin</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-reflect</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security</groupId>
|
||||||
|
<artifactId>spring-security-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger2</artifactId>
|
||||||
|
<version>2.8.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.springfox</groupId>
|
||||||
|
<artifactId>springfox-swagger-ui</artifactId>
|
||||||
|
<version>2.8.0</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||||
|
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<compilerPlugins>
|
||||||
|
<plugin>jpa</plugin>
|
||||||
|
<plugin>spring</plugin>
|
||||||
|
</compilerPlugins>
|
||||||
|
<args>
|
||||||
|
<arg>-Xjsr305=strict</arg>
|
||||||
|
</args>
|
||||||
|
</configuration>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-noarg</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-allopen</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.wbrawner.budgetserver
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class BudgetServerApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runApplication<BudgetServerApplication>(*args)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
package com.wbrawner.budgetserver
|
||||||
|
|
||||||
|
data class ErrorResponse(val message: String)
|
9
src/main/kotlin/com/wbrawner/budgetserver/Utils.kt
Normal file
9
src/main/kotlin/com/wbrawner/budgetserver/Utils.kt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package com.wbrawner.budgetserver
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.user.User
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
|
||||||
|
fun getCurrentUser(): User? {
|
||||||
|
val user = SecurityContextHolder.getContext().authentication.principal
|
||||||
|
return if (user is User) user else null
|
||||||
|
}
|
27
src/main/kotlin/com/wbrawner/budgetserver/account/Account.kt
Normal file
27
src/main/kotlin/com/wbrawner/budgetserver/account/Account.kt
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
package com.wbrawner.budgetserver.account
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.category.Category
|
||||||
|
import com.wbrawner.budgetserver.transaction.Transaction
|
||||||
|
import com.wbrawner.budgetserver.user.User
|
||||||
|
import com.wbrawner.budgetserver.user.UserResponse
|
||||||
|
import java.util.*
|
||||||
|
import javax.persistence.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Account(
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null,
|
||||||
|
val name: String = "",
|
||||||
|
val description: String? = null,
|
||||||
|
@OneToMany(mappedBy = "account") val transactions: Set<Transaction> = TreeSet(),
|
||||||
|
@OneToMany(mappedBy = "account") val categories: Set<Category> = TreeSet(),
|
||||||
|
@ManyToMany val users: Set<User> = mutableSetOf(),
|
||||||
|
@ManyToOne val owner: User
|
||||||
|
)
|
||||||
|
|
||||||
|
data class NewAccountRequest(val name: String, val description: String?, val userIds: List<Long>)
|
||||||
|
|
||||||
|
data class UpdateAccountRequest(val name: String?, val description: String?, val userIds: List<Long>?)
|
||||||
|
|
||||||
|
data class AccountResponse(val id: Long, val name: String, val description: String?, val users: List<Long>) {
|
||||||
|
constructor(account: Account) : this(account.id!!, account.name, account.description, account.users.map { it.id!! })
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.wbrawner.budgetserver.account
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.getCurrentUser
|
||||||
|
import com.wbrawner.budgetserver.user.UserRepository
|
||||||
|
import io.swagger.annotations.Api
|
||||||
|
import io.swagger.annotations.ApiOperation
|
||||||
|
import org.hibernate.Hibernate
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import javax.transaction.Transactional
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/accounts")
|
||||||
|
@Api(value = "Accounts", tags = ["Accounts"])
|
||||||
|
class AccountController @Autowired constructor(private val accountRepository: AccountRepository, private val userRepository: UserRepository) {
|
||||||
|
@Transactional
|
||||||
|
@GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "getAccounts", nickname = "getAccounts", tags = ["Accounts"])
|
||||||
|
fun getAccounts(): ResponseEntity<List<AccountResponse>> = ResponseEntity.ok(
|
||||||
|
accountRepository.findAllByUsersContainsOrOwner(getCurrentUser()!!).map {
|
||||||
|
Hibernate.initialize(it.users)
|
||||||
|
AccountResponse(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "getAccount", nickname = "getAccount", tags = ["Accounts"])
|
||||||
|
fun getAccount(@PathVariable id: Long): ResponseEntity<AccountResponse> = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, id)
|
||||||
|
.orElse(null)
|
||||||
|
?.let {
|
||||||
|
Hibernate.initialize(it.users)
|
||||||
|
ResponseEntity.ok(AccountResponse(it))
|
||||||
|
} ?: ResponseEntity.notFound().build()
|
||||||
|
|
||||||
|
@PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "newAccount", nickname = "newAccount", tags = ["Accounts"])
|
||||||
|
fun newAccount(@RequestBody request: NewAccountRequest): ResponseEntity<AccountResponse> {
|
||||||
|
val users = request.userIds
|
||||||
|
.map { id -> userRepository.findById(id).orElse(null) }
|
||||||
|
.filter { user -> user != null }
|
||||||
|
.toMutableSet()
|
||||||
|
.apply { this.add(getCurrentUser()) }
|
||||||
|
val account = accountRepository.save(Account(name = request.name, description = request.description, users = users, owner = getCurrentUser()!!))
|
||||||
|
return ResponseEntity.ok(AccountResponse(account))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "updateAccount", nickname = "updateAccount", tags = ["Accounts"])
|
||||||
|
fun updateAccount(@PathVariable id: Long, request: UpdateAccountRequest): ResponseEntity<AccountResponse> {
|
||||||
|
var account = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, id).orElse(null) ?: return ResponseEntity.notFound().build()
|
||||||
|
if (request.name != null) account = account.copy(name = request.name)
|
||||||
|
if (request.description != null) account = account.copy(description = request.description)
|
||||||
|
if (request.userIds != null) account = account.copy(users = userRepository.findAllById(request.userIds).toSet())
|
||||||
|
return ResponseEntity.ok(AccountResponse(accountRepository.save(account)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE])
|
||||||
|
@ApiOperation(value = "deleteAccount", nickname = "deleteAccount", tags = ["Accounts"])
|
||||||
|
fun deleteAccount(@PathVariable id: Long): ResponseEntity<Unit> {
|
||||||
|
val account = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, id).orElse(null) ?: return ResponseEntity.notFound().build()
|
||||||
|
accountRepository.delete(account)
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package com.wbrawner.budgetserver.account
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.category.Category
|
||||||
|
import com.wbrawner.budgetserver.transaction.Transaction
|
||||||
|
import com.wbrawner.budgetserver.user.User
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface AccountRepository: PagingAndSortingRepository<Account, Long> {
|
||||||
|
fun findAllByUsersContainsOrOwner(user: User, owner: User = user): List<Account>
|
||||||
|
fun findByUsersContainsAndId(user: User, id: Long): Optional<Account>
|
||||||
|
fun findByUsersContainsAndTransactionsContains(user: User, transaction: Transaction): Optional<Account>
|
||||||
|
fun findByUsersContainsAndCategoriesContains(user: User, category: Category): Optional<Account>
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package com.wbrawner.budgetserver.category
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.account.Account
|
||||||
|
import com.wbrawner.budgetserver.account.AccountResponse
|
||||||
|
import com.wbrawner.budgetserver.transaction.Transaction
|
||||||
|
import javax.persistence.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Category(
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null,
|
||||||
|
val title: String = "",
|
||||||
|
val description: String? = null,
|
||||||
|
val amount: Long = 0,
|
||||||
|
@ManyToOne val account: Account,
|
||||||
|
@OneToMany(mappedBy = "category") val transactions: List<Transaction> = emptyList()
|
||||||
|
) : Comparable<Category> {
|
||||||
|
override fun compareTo(other: Category): Int = title.compareTo(other.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CategoryResponse(
|
||||||
|
val id: Long,
|
||||||
|
val title: String,
|
||||||
|
val description: String?,
|
||||||
|
val amount: Long,
|
||||||
|
val accountId: Long
|
||||||
|
) {
|
||||||
|
constructor(category: Category) : this(
|
||||||
|
category.id!!,
|
||||||
|
category.title,
|
||||||
|
category.description,
|
||||||
|
category.amount,
|
||||||
|
category.account.id!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NewCategoryRequest(
|
||||||
|
val title: String,
|
||||||
|
val description: String?,
|
||||||
|
val amount: Long,
|
||||||
|
val accountId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateCategoryRequest(
|
||||||
|
val title: String?,
|
||||||
|
val description: String?,
|
||||||
|
val amount: Long?
|
||||||
|
)
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.wbrawner.budgetserver.category
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.ErrorResponse
|
||||||
|
import com.wbrawner.budgetserver.account.AccountRepository
|
||||||
|
import com.wbrawner.budgetserver.getCurrentUser
|
||||||
|
import com.wbrawner.budgetserver.transaction.TransactionRepository
|
||||||
|
import io.swagger.annotations.Api
|
||||||
|
import io.swagger.annotations.ApiOperation
|
||||||
|
import org.hibernate.Hibernate
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import java.lang.Integer.min
|
||||||
|
import javax.transaction.Transactional
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/categories")
|
||||||
|
@Api(value = "Categories", tags = ["Categories"])
|
||||||
|
class CategoryController @Autowired constructor(
|
||||||
|
private val accountRepository: AccountRepository,
|
||||||
|
private val categoryRepository: CategoryRepository,
|
||||||
|
private val transactionRepository: TransactionRepository
|
||||||
|
) {
|
||||||
|
@Transactional
|
||||||
|
@GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "getCategories", nickname = "getCategories", tags = ["Categories"])
|
||||||
|
fun getCategories(accountId: Long, count: Int?, page: Int?): ResponseEntity<List<CategoryResponse>> {
|
||||||
|
val account = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, accountId).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
Hibernate.initialize(account.users)
|
||||||
|
val pageRequest = PageRequest.of(min(0, page?.minus(1)?: 0), count?: 1000)
|
||||||
|
return ResponseEntity.ok(categoryRepository.findAllByAccount(account, pageRequest).map { CategoryResponse(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "getCategory", nickname = "getCategory", tags = ["Categories"])
|
||||||
|
fun getCategory(@PathVariable id: Long): ResponseEntity<CategoryResponse> {
|
||||||
|
val category = categoryRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build()
|
||||||
|
accountRepository.findByUsersContainsAndCategoriesContains(getCurrentUser()!!, category).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
return ResponseEntity.ok(CategoryResponse(category))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "newCategory", nickname = "newCategory", tags = ["Categories"])
|
||||||
|
fun newCategory(@RequestBody request: NewCategoryRequest): ResponseEntity<Any> {
|
||||||
|
val account = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, request.accountId).orElse(null)
|
||||||
|
?: return ResponseEntity.badRequest().body(ErrorResponse("Invalid account ID"))
|
||||||
|
Hibernate.initialize(account.users)
|
||||||
|
return ResponseEntity.ok(CategoryResponse(categoryRepository.save(Category(
|
||||||
|
title = request.title,
|
||||||
|
description = request.description,
|
||||||
|
amount = request.amount,
|
||||||
|
account = account
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "updateCategory", nickname = "updateCategory", tags = ["Categories"])
|
||||||
|
fun updateCategory(@PathVariable id: Long, @RequestBody request: UpdateCategoryRequest): ResponseEntity<CategoryResponse> {
|
||||||
|
var category = categoryRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build()
|
||||||
|
accountRepository.findByUsersContainsAndCategoriesContains(getCurrentUser()!!, category).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
request.title?.let { category = category.copy(title = it) }
|
||||||
|
request.description?.let { category = category.copy(description = it) }
|
||||||
|
request.amount?.let { category = category.copy(amount = it) }
|
||||||
|
return ResponseEntity.ok(CategoryResponse(categoryRepository.save(category)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE])
|
||||||
|
@ApiOperation(value = "deleteCategory", nickname = "deleteCategory", tags = ["Categories"])
|
||||||
|
fun deleteCategory(@PathVariable id: Long): ResponseEntity<Unit> {
|
||||||
|
val category = categoryRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build()
|
||||||
|
val account = accountRepository.findByUsersContainsAndCategoriesContains(getCurrentUser()!!, category).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
categoryRepository.delete(category)
|
||||||
|
transactionRepository.findAllByAccountAndCategory(account, category).forEach { transaction ->
|
||||||
|
transactionRepository.save(transaction.copy(category = null))
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.wbrawner.budgetserver.category
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.account.Account
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface CategoryRepository: PagingAndSortingRepository<Category, Long> {
|
||||||
|
fun findAllByAccount(account: Account, pageable: Pageable): List<Category>
|
||||||
|
fun findByAccountAndId(account: Account, id: Long): Optional<Category>
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.wbrawner.budgetserver.config
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.user.UserRepository
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class JdbcUserDetailsService @Autowired
|
||||||
|
constructor(private val userRepository: UserRepository) : UserDetailsService {
|
||||||
|
|
||||||
|
@Throws(UsernameNotFoundException::class)
|
||||||
|
override fun loadUserByUsername(username: String): UserDetails {
|
||||||
|
return userRepository.findByName(username).orElse(null)
|
||||||
|
?: throw UsernameNotFoundException("Unable to find user with username $username")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package com.wbrawner.budgetserver.config
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.passwordresetrequest.PasswordResetRequestRepository
|
||||||
|
import com.wbrawner.budgetserver.user.UserRepository
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.core.env.Environment
|
||||||
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
|
||||||
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.builders.WebSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.security.provisioning.JdbcUserDetailsManager
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
class SecurityConfig @Autowired
|
||||||
|
constructor(
|
||||||
|
private val env: Environment,
|
||||||
|
private val datasource: DataSource,
|
||||||
|
private val userRepository: UserRepository,
|
||||||
|
private val passwordResetRequestRepository: PasswordResetRequestRepository,
|
||||||
|
private val userDetailsService: JdbcUserDetailsService) : WebSecurityConfigurerAdapter() {
|
||||||
|
|
||||||
|
val userDetailsManager: JdbcUserDetailsManager
|
||||||
|
@Bean
|
||||||
|
get() {
|
||||||
|
val userDetailsManager = JdbcUserDetailsManager()
|
||||||
|
userDetailsManager.setDataSource(datasource)
|
||||||
|
return userDetailsManager
|
||||||
|
}
|
||||||
|
|
||||||
|
val authenticationProvider: DaoAuthenticationProvider
|
||||||
|
@Bean
|
||||||
|
get() = DaoAuthenticationProvider().apply {
|
||||||
|
this.setPasswordEncoder(passwordEncoder)
|
||||||
|
this.setUserDetailsService(userDetailsService)
|
||||||
|
}
|
||||||
|
|
||||||
|
val passwordEncoder: PasswordEncoder
|
||||||
|
@Bean
|
||||||
|
get() = BCryptPasswordEncoder()
|
||||||
|
|
||||||
|
public override fun configure(auth: AuthenticationManagerBuilder?) {
|
||||||
|
auth!!.authenticationProvider(authenticationProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun configure(web: WebSecurity?) {
|
||||||
|
web?.ignoring()?.antMatchers("/users/new", "/v2/api-docs")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
public override fun configure(http: HttpSecurity) {
|
||||||
|
http.authorizeRequests()
|
||||||
|
.antMatchers("/users/new", "/login")
|
||||||
|
.permitAll()
|
||||||
|
.anyRequest()
|
||||||
|
.authenticated()
|
||||||
|
.and()
|
||||||
|
.httpBasic()
|
||||||
|
.and()
|
||||||
|
.cors()
|
||||||
|
.and()
|
||||||
|
.csrf()
|
||||||
|
.disable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||||
|
class MethodSecurity : GlobalMethodSecurityConfiguration()
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.wbrawner.budgetserver.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
|
||||||
|
import springfox.documentation.builders.RequestHandlerSelectors
|
||||||
|
import springfox.documentation.spi.DocumentationType
|
||||||
|
import springfox.documentation.spring.web.plugins.Docket
|
||||||
|
import springfox.documentation.swagger2.annotations.EnableSwagger2
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableSwagger2
|
||||||
|
class SwaggerConfig : WebMvcConfigurationSupport() {
|
||||||
|
@Bean
|
||||||
|
fun budgetApi(): Docket = Docket(DocumentationType.SWAGGER_2)
|
||||||
|
.select()
|
||||||
|
.apis(RequestHandlerSelectors.basePackage("com.wbrawner.budgetserver"))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun addResourceHandlers(registry: ResourceHandlerRegistry) {
|
||||||
|
registry.addResourceHandler("swagger-ui.html")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/")
|
||||||
|
|
||||||
|
registry.addResourceHandler("/webjars/**")
|
||||||
|
.addResourceLocations("classpath:/META-INF/resources/webjars/")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.wbrawner.budgetserver.passwordresetrequest
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.user.User
|
||||||
|
import java.util.*
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.GeneratedValue
|
||||||
|
import javax.persistence.GenerationType
|
||||||
|
import javax.persistence.Id
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class PasswordResetRequest(
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null,
|
||||||
|
val user: User? = null,
|
||||||
|
val date: Calendar = GregorianCalendar(),
|
||||||
|
val token: String = UUID.randomUUID().toString().replace("-", "")
|
||||||
|
)
|
|
@ -0,0 +1,5 @@
|
||||||
|
package com.wbrawner.budgetserver.passwordresetrequest
|
||||||
|
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository
|
||||||
|
|
||||||
|
interface PasswordResetRequestRepository: PagingAndSortingRepository<PasswordResetRequest, Long>
|
|
@ -0,0 +1,67 @@
|
||||||
|
package com.wbrawner.budgetserver.transaction
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.account.Account
|
||||||
|
import com.wbrawner.budgetserver.category.Category
|
||||||
|
import com.wbrawner.budgetserver.user.User
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.persistence.*
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Transaction(
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null,
|
||||||
|
val title: String = "",
|
||||||
|
val description: String? = null,
|
||||||
|
val date: Instant = Instant.now(),
|
||||||
|
val amount: Long = 0,
|
||||||
|
@ManyToOne val category: Category? = null,
|
||||||
|
val isExpense: Boolean = true,
|
||||||
|
@ManyToOne val createdBy: User,
|
||||||
|
@ManyToOne val account: Account
|
||||||
|
) : Comparable<Transaction> {
|
||||||
|
override fun compareTo(other: Transaction): Int = this.date.compareTo(other.date)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TransactionResponse(
|
||||||
|
val id: Long,
|
||||||
|
val title: String,
|
||||||
|
val description: String?,
|
||||||
|
val date: String,
|
||||||
|
val amount: Long,
|
||||||
|
val isExpense: Boolean,
|
||||||
|
val accountId: Long,
|
||||||
|
val categoryId: Long?,
|
||||||
|
val createdBy: Long
|
||||||
|
) {
|
||||||
|
constructor(transaction: Transaction) : this(
|
||||||
|
transaction.id!!,
|
||||||
|
transaction.title,
|
||||||
|
transaction.description,
|
||||||
|
transaction.date.toString(),
|
||||||
|
transaction.amount,
|
||||||
|
transaction.isExpense,
|
||||||
|
transaction.account.id!!,
|
||||||
|
if (transaction.category != null) transaction.category.id!! else null,
|
||||||
|
transaction.createdBy.id!!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NewTransactionRequest(
|
||||||
|
val title: String,
|
||||||
|
val description: String?,
|
||||||
|
val date: String,
|
||||||
|
val amount: Long,
|
||||||
|
val categoryId: Long?,
|
||||||
|
val isExpense: Boolean,
|
||||||
|
val accountId: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateTransactionRequest(
|
||||||
|
val title: String?,
|
||||||
|
val description: String?,
|
||||||
|
val date: String?,
|
||||||
|
val amount: Long?,
|
||||||
|
val categoryId: Long?,
|
||||||
|
val isExpense: Boolean?,
|
||||||
|
val accountId: Long?,
|
||||||
|
val createdBy: Long?
|
||||||
|
)
|
|
@ -0,0 +1,108 @@
|
||||||
|
package com.wbrawner.budgetserver.transaction
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.ErrorResponse
|
||||||
|
import com.wbrawner.budgetserver.account.AccountRepository
|
||||||
|
import com.wbrawner.budgetserver.category.Category
|
||||||
|
import com.wbrawner.budgetserver.category.CategoryRepository
|
||||||
|
import com.wbrawner.budgetserver.getCurrentUser
|
||||||
|
import io.swagger.annotations.Api
|
||||||
|
import io.swagger.annotations.ApiOperation
|
||||||
|
import org.hibernate.Hibernate
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import java.lang.Integer.min
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
import javax.transaction.Transactional
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/transactions")
|
||||||
|
@Api(value = "Transactions", tags = ["Transactions"])
|
||||||
|
class TransactionController @Autowired constructor(
|
||||||
|
private val accountRepository: AccountRepository,
|
||||||
|
private val categoryRepository: CategoryRepository,
|
||||||
|
private val transactionRepository: TransactionRepository
|
||||||
|
) {
|
||||||
|
@Transactional
|
||||||
|
@GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "getTransactions", nickname = "getTransactions", tags = ["Transactions"])
|
||||||
|
fun getTransactions(accountId: Long, count: Int?, page: Int?): ResponseEntity<List<TransactionResponse>> {
|
||||||
|
val account = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, accountId).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
Hibernate.initialize(account.users)
|
||||||
|
val pageRequest = PageRequest.of(min(0, page?.minus(1)?: 0), count?: 1000)
|
||||||
|
return ResponseEntity.ok(transactionRepository.findAllByAccount(account, pageRequest).map { TransactionResponse(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "getTransaction", nickname = "getTransaction", tags = ["Transactions"])
|
||||||
|
fun getTransaction(@PathVariable id: Long): ResponseEntity<TransactionResponse> {
|
||||||
|
val transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build()
|
||||||
|
accountRepository.findByUsersContainsAndTransactionsContains(getCurrentUser()!!, transaction).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
return ResponseEntity.ok(TransactionResponse(transaction))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "newTransaction", nickname = "newTransaction", tags = ["Transactions"])
|
||||||
|
fun newTransaction(@RequestBody request: NewTransactionRequest): ResponseEntity<Any> {
|
||||||
|
val account = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, request.accountId).orElse(null)
|
||||||
|
?: return ResponseEntity.badRequest().body(ErrorResponse("Invalid account ID"))
|
||||||
|
Hibernate.initialize(account.users)
|
||||||
|
val category: Category? = request.categoryId?.let {
|
||||||
|
categoryRepository.findByAccountAndId(account, request.categoryId).orElse(null)
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(TransactionResponse(transactionRepository.save(Transaction(
|
||||||
|
title = request.title,
|
||||||
|
description = request.description,
|
||||||
|
date = Instant.parse(request.date),
|
||||||
|
amount = request.amount,
|
||||||
|
category = category,
|
||||||
|
isExpense = request.isExpense,
|
||||||
|
account = account,
|
||||||
|
createdBy = getCurrentUser()!!
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "updateTransaction", nickname = "updateTransaction", tags = ["Transactions"])
|
||||||
|
fun updateTransaction(@PathVariable id: Long, @RequestBody request: UpdateTransactionRequest): ResponseEntity<TransactionResponse> {
|
||||||
|
var transaction = transactionRepository.findById(id).orElse(null)?: return ResponseEntity.notFound().build()
|
||||||
|
var account = accountRepository.findByUsersContainsAndTransactionsContains(getCurrentUser()!!, transaction)
|
||||||
|
.orElse(null)?: return ResponseEntity.notFound().build()
|
||||||
|
request.title?.let { transaction = transaction.copy(title = it) }
|
||||||
|
request.description?.let { transaction = transaction.copy(description = it) }
|
||||||
|
request.date?.let { transaction = transaction.copy(date = Instant.parse(it)) }
|
||||||
|
request.amount?.let { transaction = transaction.copy(amount = it) }
|
||||||
|
request.isExpense?.let { transaction = transaction.copy(isExpense = it) }
|
||||||
|
request.accountId?.let { accountId ->
|
||||||
|
accountRepository.findByUsersContainsAndId(getCurrentUser()!!, accountId).orElse(null)?.let {
|
||||||
|
account = it
|
||||||
|
transaction = transaction.copy(account = it, category = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
request.categoryId?.let {
|
||||||
|
categoryRepository.findByAccountAndId(account, it).orElse(null)?.let { category ->
|
||||||
|
transaction = transaction.copy(category = category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(TransactionResponse(transactionRepository.save(transaction)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE])
|
||||||
|
@ApiOperation(value = "deleteTransaction", nickname = "deleteTransaction", tags = ["Transactions"])
|
||||||
|
fun deleteTransaction(@PathVariable id: Long): ResponseEntity<Unit> {
|
||||||
|
val transaction = transactionRepository.findById(id).orElse(null) ?: return ResponseEntity.notFound().build()
|
||||||
|
// Check that the transaction belongs to an account that the user has access to before deleting it
|
||||||
|
accountRepository.findByUsersContainsAndTransactionsContains(getCurrentUser()!!, transaction).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
transactionRepository.delete(transaction)
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package com.wbrawner.budgetserver.transaction
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.account.Account
|
||||||
|
import com.wbrawner.budgetserver.category.Category
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface TransactionRepository: PagingAndSortingRepository<Transaction, Long> {
|
||||||
|
fun findAllByAccount(account: Account, pageable: Pageable): List<Transaction>
|
||||||
|
fun findByAccountAndId(account: Account, id: Long): Optional<Transaction>
|
||||||
|
fun findAllByAccountAndCategory(account: Account, category: Category): List<Transaction>
|
||||||
|
}
|
45
src/main/kotlin/com/wbrawner/budgetserver/user/User.kt
Normal file
45
src/main/kotlin/com/wbrawner/budgetserver/user/User.kt
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package com.wbrawner.budgetserver.user
|
||||||
|
|
||||||
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.GeneratedValue
|
||||||
|
import javax.persistence.GenerationType
|
||||||
|
import javax.persistence.Id
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class User(
|
||||||
|
@Id @GeneratedValue(strategy = GenerationType.AUTO) val id: Long? = null,
|
||||||
|
val name: String = "",
|
||||||
|
val passphrase: String = "",
|
||||||
|
val email: String = "",
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
val credentialsExpired: Boolean = false,
|
||||||
|
val isExpired: Boolean = false,
|
||||||
|
val isLocked: Boolean = false,
|
||||||
|
@Transient val grantedAuthorities: MutableCollection<out GrantedAuthority>
|
||||||
|
= mutableListOf<GrantedAuthority>(SimpleGrantedAuthority("USER"))
|
||||||
|
) : UserDetails {
|
||||||
|
override fun getUsername(): String = name
|
||||||
|
|
||||||
|
override fun getAuthorities(): MutableCollection<out GrantedAuthority> = grantedAuthorities
|
||||||
|
|
||||||
|
override fun isEnabled(): Boolean = enabled
|
||||||
|
|
||||||
|
override fun isCredentialsNonExpired(): Boolean = !credentialsExpired
|
||||||
|
|
||||||
|
override fun getPassword(): String = passphrase
|
||||||
|
|
||||||
|
override fun isAccountNonExpired(): Boolean = !isExpired
|
||||||
|
|
||||||
|
override fun isAccountNonLocked(): Boolean = !isLocked
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UserResponse(val id: Long, val username: String, val email: String) {
|
||||||
|
constructor(user: User) : this(user.id!!, user.name, user.email)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NewUserRequest(val username: String, val password: String, val email: String)
|
||||||
|
|
||||||
|
data class UpdateUserRequest(val username: String?, val password: String?, val email: String?)
|
|
@ -0,0 +1,86 @@
|
||||||
|
package com.wbrawner.budgetserver.user
|
||||||
|
|
||||||
|
import com.wbrawner.budgetserver.ErrorResponse
|
||||||
|
import com.wbrawner.budgetserver.account.AccountRepository
|
||||||
|
import com.wbrawner.budgetserver.getCurrentUser
|
||||||
|
import io.swagger.annotations.Api
|
||||||
|
import io.swagger.annotations.ApiOperation
|
||||||
|
import org.hibernate.Hibernate
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
import javax.transaction.Transactional
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/users")
|
||||||
|
@Api(value = "Users", tags = ["Users"])
|
||||||
|
class UserController @Autowired constructor(private val accountRepository: AccountRepository, private val userRepository: UserRepository, private val passwordEncoder: PasswordEncoder) {
|
||||||
|
@Transactional
|
||||||
|
@GetMapping("", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "getUsers", nickname = "getUsers", tags = ["Users"])
|
||||||
|
fun getUsers(accountId: Long): ResponseEntity<List<UserResponse>> {
|
||||||
|
val account = accountRepository.findByUsersContainsAndId(getCurrentUser()!!, accountId).orElse(null)
|
||||||
|
?: return ResponseEntity.notFound().build()
|
||||||
|
Hibernate.initialize(account.users)
|
||||||
|
return ResponseEntity.ok(account.users.map { UserResponse(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@ApiOperation(value = "getUser", nickname = "getUser", tags = ["Users"])
|
||||||
|
fun getUser(@PathVariable id: Long): ResponseEntity<UserResponse> = userRepository.findById(id).orElse(null)
|
||||||
|
?.let {
|
||||||
|
ResponseEntity.ok(UserResponse(it))
|
||||||
|
}
|
||||||
|
?: ResponseEntity.notFound().build()
|
||||||
|
|
||||||
|
@PostMapping("/new", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "newUser", nickname = "newUser", tags = ["Users"])
|
||||||
|
fun newUser(@RequestBody request: NewUserRequest): ResponseEntity<Any> {
|
||||||
|
if (userRepository.findByName(request.username).isPresent)
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ErrorResponse("Username taken"))
|
||||||
|
if (userRepository.findByEmail(request.email).isPresent)
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ErrorResponse("Email taken"))
|
||||||
|
if (request.password.isBlank())
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ErrorResponse("Invalid password"))
|
||||||
|
return ResponseEntity.ok(UserResponse(userRepository.save(User(
|
||||||
|
name = request.username,
|
||||||
|
passphrase = passwordEncoder.encode(request.password),
|
||||||
|
email = request.email
|
||||||
|
))))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}", consumes = [MediaType.APPLICATION_JSON_VALUE], produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||||
|
@ApiOperation(value = "updateUser", nickname = "updateUser", tags = ["Users"])
|
||||||
|
fun updateUser(@PathVariable id: Long, @RequestBody request: UpdateUserRequest): ResponseEntity<Any> {
|
||||||
|
if (getCurrentUser()!!.id != id) return ResponseEntity.status(403)
|
||||||
|
.body(ErrorResponse("Attempting to modify another user's account"))
|
||||||
|
var user = userRepository.findById(getCurrentUser()!!.id!!).orElse(null)?: return ResponseEntity.notFound().build()
|
||||||
|
if (request.username != null) {
|
||||||
|
if (userRepository.findByName(request.username).isPresent) throw RuntimeException("Username taken")
|
||||||
|
user = user.copy(name = request.username)
|
||||||
|
}
|
||||||
|
if (request.email != null) {
|
||||||
|
if (userRepository.findByEmail(request.email).isPresent) throw RuntimeException("Email taken")
|
||||||
|
user = user.copy(email = request.email)
|
||||||
|
}
|
||||||
|
if (request.password != null) {
|
||||||
|
if (request.password.isBlank()) throw RuntimeException("Invalid password")
|
||||||
|
user = user.copy(passphrase = passwordEncoder.encode(request.password))
|
||||||
|
}
|
||||||
|
return ResponseEntity.ok(UserResponse(userRepository.save(user)))
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}", produces = [MediaType.TEXT_PLAIN_VALUE])
|
||||||
|
@ApiOperation(value = "deleteUser", nickname = "deleteUser", tags = ["Users"])
|
||||||
|
fun deleteUser(@PathVariable id: Long): ResponseEntity<Unit> {
|
||||||
|
if(getCurrentUser()!!.id != id) return ResponseEntity.status(403).build()
|
||||||
|
userRepository.deleteById(id)
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package com.wbrawner.budgetserver.user
|
||||||
|
|
||||||
|
import org.springframework.data.repository.PagingAndSortingRepository
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface UserRepository: PagingAndSortingRepository<User, Long> {
|
||||||
|
fun findByName(username: String): Optional<User>
|
||||||
|
fun findByEmail(email: String): Optional<User>
|
||||||
|
}
|
6
src/main/resources/application.properties
Normal file
6
src/main/resources/application.properties
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
spring.jpa.hibernate.ddl-auto=create-drop
|
||||||
|
spring.datasource.url=jdbc:mysql://localhost:3306/budget
|
||||||
|
spring.datasource.username=budget
|
||||||
|
spring.datasource.password=budget
|
||||||
|
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
|
||||||
|
spring.profiles.active=dev
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.wbrawner.budgetserver
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner
|
||||||
|
|
||||||
|
@RunWith(SpringRunner::class)
|
||||||
|
@SpringBootTest
|
||||||
|
class BudgetServerApplicationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun contextLoads() {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue