From cacfd9f6c9e5499237ca8c4639a4dcf606817540 Mon Sep 17 00:00:00 2001 From: Adam Date: Thu, 13 Oct 2022 09:56:58 +0100 Subject: [PATCH] Add quickstart project --- .dockerignore | 5 + .gitignore | 39 ++ .mvn/wrapper/.gitignore | 1 + .mvn/wrapper/MavenWrapperDownloader.java | 142 +++++++ .mvn/wrapper/maven-wrapper.properties | 18 + .s2i/environment | 5 + README.md | 18 + mvnw | 316 ++++++++++++++++ mvnw.cmd | 188 ++++++++++ pom.xml | 158 ++++++++ src/main/docker/Dockerfile.jvm | 94 +++++ src/main/docker/Dockerfile.legacy-jar | 90 +++++ src/main/docker/Dockerfile.native | 27 ++ src/main/docker/Dockerfile.native-micro | 30 ++ .../enterprisemiddleware/Application.java | 25 ++ .../enterprisemiddleware/area/Area.java | 71 ++++ .../area/AreaService.java | 29 ++ .../area/InvalidAreaCodeException.java | 27 ++ .../enterprisemiddleware/contact/Contact.java | 141 +++++++ .../contact/ContactRepository.java | 177 +++++++++ .../contact/ContactRestService.java | 345 ++++++++++++++++++ .../contact/ContactService.java | 185 ++++++++++ .../contact/ContactValidator.java | 89 +++++ .../contact/UniqueEmailException.java | 27 ++ .../util/ErrorMessage.java | 36 ++ .../enterprisemiddleware/util/Resources.java | 39 ++ .../util/RestServiceException.java | 72 ++++ .../util/RestServiceExceptionMapper.java | 37 ++ .../resources/META-INF/resources/index.html | 289 +++++++++++++++ src/main/resources/application.properties | 15 + src/main/resources/import.sql | 5 + .../ContactRestServiceIntegrationTest.java | 100 +++++ 32 files changed, 2840 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .mvn/wrapper/.gitignore create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 .s2i/environment create mode 100644 README.md create mode 100755 mvnw create mode 100755 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/docker/Dockerfile.jvm create mode 100644 src/main/docker/Dockerfile.legacy-jar create mode 100644 src/main/docker/Dockerfile.native create mode 100644 src/main/docker/Dockerfile.native-micro create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/Application.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/area/Area.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/area/AreaService.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/area/InvalidAreaCodeException.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/Contact.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRepository.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestService.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactService.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactValidator.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/UniqueEmailException.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/util/ErrorMessage.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/util/Resources.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceException.java create mode 100644 src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceExceptionMapper.java create mode 100644 src/main/resources/META-INF/resources/index.html create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/import.sql create mode 100644 src/test/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestServiceIntegrationTest.java diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..94810d0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +* +!target/*-runner +!target/*-runner.jar +!target/lib/* +!target/quarkus-app/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bdf57ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +#Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties + +# Eclipse +.project +.classpath +.settings/ +bin/ + +# IntelliJ +.idea +*.ipr +*.iml +*.iws + +# NetBeans +nb-configuration.xml + +# Visual Studio Code +.vscode +.factorypath + +# OSX +.DS_Store + +# Vim +*.swp +*.swo + +# patch +*.orig +*.rej + +# Local environment +.env diff --git a/.mvn/wrapper/.gitignore b/.mvn/wrapper/.gitignore new file mode 100644 index 0000000..e72f5e8 --- /dev/null +++ b/.mvn/wrapper/.gitignore @@ -0,0 +1 @@ +maven-wrapper.jar diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..1708393 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader +{ + private static final String WRAPPER_VERSION = "3.1.1"; + + /** + * 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/org/apache/maven/wrapper/maven-wrapper/" + WRAPPER_VERSION + + "/maven-wrapper-" + WRAPPER_VERSION + ".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 directory '" + 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 + { + if ( System.getenv( "MVNW_USERNAME" ) != null && System.getenv( "MVNW_PASSWORD" ) != null ) + { + String username = System.getenv( "MVNW_USERNAME" ); + char[] password = System.getenv( "MVNW_PASSWORD" ).toCharArray(); + Authenticator.setDefault( new Authenticator() + { + @Override + protected PasswordAuthentication getPasswordAuthentication() + { + return new PasswordAuthentication( username, password ); + } + } ); + } + 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(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..61a2ef1 --- /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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-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/.s2i/environment b/.s2i/environment new file mode 100644 index 0000000..551a484 --- /dev/null +++ b/.s2i/environment @@ -0,0 +1,5 @@ +MAVEN_S2I_ARTIFACT_DIRS=target/quarkus-app +S2I_SOURCE_DEPLOYMENTS_FILTER=app lib quarkus quarkus-run.jar +JAVA_OPTIONS=-Dquarkus.http.host=0.0.0.0 +AB_JOLOKIA_OFF=true +JAVA_APP_JAR=/deployments/quarkus-run.jar \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b8e091b --- /dev/null +++ b/README.md @@ -0,0 +1,18 @@ +# CSC8104 Quickstart Project + +This quickstart project is provided for students on the CSC8104 Enterprise Middleware module and provides a foundation for starting their coursework. Students are expected to download and build their solution within the provided project and should not aim to create a new project from scratch. + +Within the project there is an example REST service for creating and storing contacts which can be accessed via the Swagger UI endpoint (http://localhost:8080/q/swagger-ui). It is encouraged that students spend spend time reading through this code to gain a strong understanding of how the project works. Not only this, but students are also encouraged to follow a similar packaging structure. + +Students are not required to remove the contact service and can leave this functionality in their submission. + +Throughout the coursework specification there are many links to various guides to help you complete the coursework. It is strongly encouraged that you spend time working through these guides before attempting to implement the specification requirements. + +## Running the application in dev mode + +You can run your application in dev mode that enables live coding using: +```shell script +./mvnw compile quarkus:dev +``` + +> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at http://localhost:8080/q/dev/. diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..eaa3d30 --- /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.1/maven-wrapper-3.1.1.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.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 100755 index 0000000..abb7c32 --- /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.1/maven-wrapper-3.1.1.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.1/maven-wrapper-3.1.1.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..f2a5e60 --- /dev/null +++ b/pom.xml @@ -0,0 +1,158 @@ + + + 4.0.0 + uk.ac.newcastle.enterprisemiddleware + csc8104 + 1.0.0-SNAPSHOT + + 3.8.1 + 11 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 2.10.3.Final + true + 3.0.0-M7 + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + io.quarkus + quarkus-resteasy-reactive-jackson + + + io.quarkus + quarkus-hibernate-orm + + + io.quarkus + quarkus-jdbc-h2 + + + io.quarkus + quarkus-narayana-jta + + + io.quarkus + quarkus-hibernate-validator + + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-rest-client-reactive-jackson + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy-reactive + + + io.quarkus + quarkus-openshift + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + io.quarkus + quarkus-test-h2 + test + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + maven-surefire-plugin + ${surefire-plugin.version} + + + org.jboss.logmanager.LogManager + ${maven.home} + + + + + maven-failsafe-plugin + ${surefire-plugin.version} + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + org.jboss.logmanager.LogManager + ${maven.home} + + + + + + + + + + native + + + native + + + + false + native + + + + diff --git a/src/main/docker/Dockerfile.jvm b/src/main/docker/Dockerfile.jvm new file mode 100644 index 0000000..d8e5a34 --- /dev/null +++ b/src/main/docker/Dockerfile.jvm @@ -0,0 +1,94 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/csc8104-jvm . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/csc8104-jvm +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/csc8104-jvm +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.11 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + + +# We make four distinct layers so if there are application changes the library layers can be re-used +COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/ +COPY --chown=185 target/quarkus-app/*.jar /deployments/ +COPY --chown=185 target/quarkus-app/app/ /deployments/app/ +COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/ + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" + diff --git a/src/main/docker/Dockerfile.legacy-jar b/src/main/docker/Dockerfile.legacy-jar new file mode 100644 index 0000000..3e533e8 --- /dev/null +++ b/src/main/docker/Dockerfile.legacy-jar @@ -0,0 +1,90 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode +# +# Before building the container image run: +# +# ./mvnw package -Dquarkus.package.type=legacy-jar +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/csc8104-legacy-jar . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/csc8104-legacy-jar +# +# If you want to include the debug port into your docker image +# you will have to expose the debug port (default 5005) like this : EXPOSE 8080 5005 +# +# Then run the container using : +# +# docker run -i --rm -p 8080:8080 quarkus/csc8104-legacy-jar +# +# This image uses the `run-java.sh` script to run the application. +# This scripts computes the command line to execute your Java application, and +# includes memory/GC tuning. +# You can configure the behavior using the following environment properties: +# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") +# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options +# in JAVA_OPTS (example: "-Dsome.property=foo") +# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is +# used to calculate a default maximal heap memory based on a containers restriction. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio +# of the container available memory as set here. The default is `50` which means 50% +# of the available memory is used as an upper boundary. You can skip this mechanism by +# setting this value to `0` in which case no `-Xmx` option is added. +# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This +# is used to calculate a default initial heap memory based on the maximum heap memory. +# If used in a container without any memory constraints for the container then this +# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio +# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx` +# is used as the initial heap size. You can skip this mechanism by setting this value +# to `0` in which case no `-Xms` option is added (example: "25") +# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS. +# This is used to calculate the maximum value of the initial heap memory. If used in +# a container without any memory constraints for the container then this option has +# no effect. If there is a memory constraint then `-Xms` is limited to the value set +# here. The default is 4096MB which means the calculated value of `-Xms` never will +# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096") +# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output +# when things are happening. This option, if set to true, will set +# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true"). +# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example: +# true"). +# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787"). +# - CONTAINER_CORE_LIMIT: A calculated core limit as described in +# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2") +# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024"). +# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion. +# (example: "20") +# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking. +# (example: "40") +# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection. +# (example: "4") +# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus +# previous GC times. (example: "90") +# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20") +# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100") +# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should +# contain the necessary JRE command-line options to specify the required GC, which +# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC). +# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080") +# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080") +# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be +# accessed directly. (example: "foo.example.com,bar.example.com") +# +### +FROM registry.access.redhat.com/ubi8/openjdk-11:1.11 + +ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' + + +COPY target/lib/* /deployments/lib/ +COPY target/*-runner.jar /deployments/quarkus-run.jar + +EXPOSE 8080 +USER 185 +ENV AB_JOLOKIA_OFF="" +ENV JAVA_OPTS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager" +ENV JAVA_APP_JAR="/deployments/quarkus-run.jar" diff --git a/src/main/docker/Dockerfile.native b/src/main/docker/Dockerfile.native new file mode 100644 index 0000000..d5144ad --- /dev/null +++ b/src/main/docker/Dockerfile.native @@ -0,0 +1,27 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native -t quarkus/csc8104 . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/csc8104 +# +### +FROM registry.access.redhat.com/ubi8/ubi-minimal:8.5 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/docker/Dockerfile.native-micro b/src/main/docker/Dockerfile.native-micro new file mode 100644 index 0000000..0a47b2c --- /dev/null +++ b/src/main/docker/Dockerfile.native-micro @@ -0,0 +1,30 @@ +#### +# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode. +# It uses a micro base image, tuned for Quarkus native executables. +# It reduces the size of the resulting container image. +# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image. +# +# Before building the container image run: +# +# ./mvnw package -Pnative +# +# Then, build the image with: +# +# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/csc8104 . +# +# Then run the container using: +# +# docker run -i --rm -p 8080:8080 quarkus/csc8104 +# +### +FROM quay.io/quarkus/quarkus-micro-image:1.0 +WORKDIR /work/ +RUN chown 1001 /work \ + && chmod "g+rwX" /work \ + && chown 1001:root /work +COPY --chown=1001:root target/*-runner /work/application + +EXPOSE 8080 +USER 1001 + +CMD ["./application", "-Dquarkus.http.host=0.0.0.0"] diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/Application.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/Application.java new file mode 100644 index 0000000..b42b4e0 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/Application.java @@ -0,0 +1,25 @@ +package uk.ac.newcastle.enterprisemiddleware; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.annotations.QuarkusMain; +import org.h2.tools.Server; + +import java.sql.SQLException; + +@QuarkusMain +public class Application { + + private static Server server; + public static void main(String[] args) { + // Start H2 in server mode to allow remote connections (DBeaver) + try { + server = Server.createTcpServer("-tcpPort", "9092", "-tcpAllowOthers", "-ifNotExists").start(); + } catch (SQLException e) { + throw new RuntimeException("Could not start H2 server", e); + } + + // Start the Quarkus app + Quarkus.run(args); + } + +} diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/Area.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/Area.java new file mode 100644 index 0000000..1c562f9 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/Area.java @@ -0,0 +1,71 @@ +package uk.ac.newcastle.enterprisemiddleware.area; + +import java.io.Serializable; +/** + *

Simple POJO representing AreaCode objects

+ * + * @author hugofirth + */ +public class Area implements Serializable { + + private static final long serialVersionUID = 249872301293L; + + private int id; + private String state; + private String abbr; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + public String getAbbr() { + return abbr; + } + + public void setAbbr(String abbr) { + this.abbr = abbr; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Area)) return false; + + Area area = (Area) o; + + if (id != area.id) return false; + if (!state.equals(area.state)) return false; + return abbr.equals(area.abbr); + + } + + @Override + public int hashCode() { + int result = id; + result = 31 * result + state.hashCode(); + result = 31 * result + abbr.hashCode(); + return result; + } + + @Override + public String toString() { + return "Area{" + + "id=" + id + + ", state='" + state + '\'' + + ", abbr='" + abbr + '\'' + + '}'; + } +} + diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/AreaService.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/AreaService.java new file mode 100644 index 0000000..4c2f3f0 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/AreaService.java @@ -0,0 +1,29 @@ +package uk.ac.newcastle.enterprisemiddleware.area; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import javax.enterprise.context.Dependent; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import java.util.List; + +/** + *

Clientside representation of an AreaCode object pulled from an external RESTFul API.

+ * + *

This is the mirror opposite of a server side JAX-RS service

+ * + * @author hugofirth + */ +@Path("/areas") +@RegisterRestClient(configKey = "area-api") +public interface AreaService { + + @GET + List getAreas(); + + + @GET + @Path("/{id:[0-9]+}") + Area getAreaById(@PathParam("id") int id); +} \ No newline at end of file diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/InvalidAreaCodeException.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/InvalidAreaCodeException.java new file mode 100644 index 0000000..8127ce9 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/area/InvalidAreaCodeException.java @@ -0,0 +1,27 @@ +package uk.ac.newcastle.enterprisemiddleware.area; + +import javax.validation.ValidationException; + +/** + *

ValidationException which should be thrown if a non-existant US Area code is provided to AreaService.

+ * + *

In such cases the ClientResponse status should be 404 NOT_FOUND.

+ * + * @author hugofirth + * @see AreaService + */ +public class InvalidAreaCodeException extends ValidationException { + + public InvalidAreaCodeException(Throwable cause) { + super(cause); + } + + public InvalidAreaCodeException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidAreaCodeException(String message) { + super(message); + } +} + diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/Contact.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/Contact.java new file mode 100644 index 0000000..c7ec1cc --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/Contact.java @@ -0,0 +1,141 @@ +package uk.ac.newcastle.enterprisemiddleware.contact; + +import javax.persistence.*; +import javax.validation.constraints.*; +import javax.xml.bind.annotation.XmlRootElement; +import java.io.Serializable; +import java.util.Date; +import java.util.Objects; + +/** + *

This is a the Domain object. The Contact class represents how contact resources are represented in the application + * database.

+ * + *

The class also specifies how a contacts are retrieved from the database (with @NamedQueries), and acceptable values + * for Contact fields (with @NotNull, @Pattern etc...)

+ * + * @author Joshua Wilson + */ +/* + * The @NamedQueries included here are for searching against the table that reflects this object. This is the most efficient + * form of query in JPA though is it more error prone due to the syntax being in a String. This makes it harder to debug. + */ +@Entity +@NamedQueries({ + @NamedQuery(name = Contact.FIND_ALL, query = "SELECT c FROM Contact c ORDER BY c.lastName ASC, c.firstName ASC"), + @NamedQuery(name = Contact.FIND_BY_EMAIL, query = "SELECT c FROM Contact c WHERE c.email = :email") +}) +@XmlRootElement +@Table(name = "contact", uniqueConstraints = @UniqueConstraint(columnNames = "email")) +public class Contact implements Serializable { + /** Default value included to remove warning. Remove or modify at will. **/ + private static final long serialVersionUID = 1L; + + public static final String FIND_ALL = "Contact.findAll"; + public static final String FIND_BY_EMAIL = "Contact.findByEmail"; + + @Id + @GeneratedValue(strategy = GenerationType.TABLE) + private Long id; + + @NotNull + @Size(min = 1, max = 25) + @Pattern(regexp = "[A-Za-z-']+", message = "Please use a name without numbers or specials") + @Column(name = "first_name") + private String firstName; + + @NotNull + @Size(min = 1, max = 25) + @Pattern(regexp = "[A-Za-z-']+", message = "Please use a name without numbers or specials") + @Column(name = "last_name") + private String lastName; + + @NotNull + @NotEmpty + @Email(message = "The email address must be in the format of name@domain.com") + private String email; + + @NotNull + @Pattern(regexp = "^\\([2-9][0-8][0-9]\\)\\s?[0-9]{3}\\-[0-9]{4}$") + @Column(name = "phone_number") + private String phoneNumber; + + @NotNull + @Past(message = "Birthdates can not be in the future. Please choose one from the past") + @Column(name = "birth_date") + @Temporal(TemporalType.DATE) + private Date birthDate; + + @Column(name = "state") + private String state; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhoneNumber() { + return phoneNumber; + } + + public void setPhoneNumber(String phoneNumber) { + this.phoneNumber = phoneNumber; + } + + public Date getBirthDate() { + return birthDate; + } + + public void setBirthDate(Date birthDate) { + this.birthDate = birthDate; + } + + public void setState(String state) { + this.state = state; + } + + public String getState() { + return this.state; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Contact)) return false; + Contact contact = (Contact) o; + return email.equals(contact.email); + } + + @Override + public int hashCode() { + return Objects.hashCode(email); + } +} + diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRepository.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRepository.java new file mode 100644 index 0000000..cbc66a8 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRepository.java @@ -0,0 +1,177 @@ +package uk.ac.newcastle.enterprisemiddleware.contact; + +import javax.enterprise.context.RequestScoped; +import javax.inject.Inject; +import javax.inject.Named; +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; +import javax.validation.ConstraintViolationException; +import java.util.List; +import java.util.logging.Logger; + +/** + *

This is a Repository class and connects the Service/Control layer (see {@link ContactService} with the + * Domain/Entity Object (see {@link Contact}).

+ * + *

There are no access modifiers on the methods making them 'package' scope. They should only be accessed by a + * Service/Control object.

+ * + * @author Joshua Wilson + * @see Contact + * @see javax.persistence.EntityManager + */ +@RequestScoped +public class ContactRepository { + + @Inject + @Named("logger") + Logger log; + + @Inject + EntityManager em; + + /** + *

Returns a List of all persisted {@link Contact} objects, sorted alphabetically by last name.

+ * + * @return List of Contact objects + */ + List findAllOrderedByName() { + TypedQuery query = em.createNamedQuery(Contact.FIND_ALL, Contact.class); + return query.getResultList(); + } + + /** + *

Returns a single Contact object, specified by a Long id.

+ * + * @param id The id field of the Contact to be returned + * @return The Contact with the specified id + */ + Contact findById(Long id) { + return em.find(Contact.class, id); + } + + /** + *

Returns a single Contact object, specified by a String email.

+ * + *

If there is more than one Contact with the specified email, only the first encountered will be returned.

+ * + * @param email The email field of the Contact to be returned + * @return The first Contact with the specified email + */ + Contact findByEmail(String email) { + TypedQuery query = em.createNamedQuery(Contact.FIND_BY_EMAIL, Contact.class).setParameter("email", email); + return query.getSingleResult(); + } + + /** + *

Returns a list of Contact objects, specified by a String firstName.

+ * + * @param firstName The firstName field of the Contacts to be returned + * @return The Contacts with the specified firstName + */ + List findAllByFirstName(String firstName) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery criteria = cb.createQuery(Contact.class); + Root contact = criteria.from(Contact.class); + // Swap criteria statements if you would like to try out type-safe criteria queries, a new feature in JPA 2.0. + // criteria.select(contact).where(cb.equal(contact.get(Contact_.firstName), firstName)); + criteria.select(contact).where(cb.equal(contact.get("firstName"), firstName)); + return em.createQuery(criteria).getResultList(); + } + + /** + *

Returns a single Contact object, specified by a String lastName.

+ * + * @param lastName The lastName field of the Contacts to be returned + * @return The Contacts with the specified lastName + */ + List findAllByLastName(String lastName) { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery criteria = cb.createQuery(Contact.class); + Root contact = criteria.from(Contact.class); + // Swap criteria statements if you would like to try out type-safe criteria queries, a new feature in JPA 2.0. + // criteria.select(contact).where(cb.equal(contact.get(Contact_.lastName), lastName)); + criteria.select(contact).where(cb.equal(contact.get("lastName"), lastName)); + return em.createQuery(criteria).getResultList(); + } + + /** + *

Persists the provided Contact object to the application database using the EntityManager.

+ * + *

{@link javax.persistence.EntityManager#persist(Object) persist(Object)} takes an entity instance, adds it to the + * context and makes that instance managed (ie future updates to the entity will be tracked)

+ * + *

persist(Object) will set the @GeneratedValue @Id for an object.

+ * + * @param contact The Contact object to be persisted + * @return The Contact object that has been persisted + * @throws ConstraintViolationException, ValidationException, Exception + */ + Contact create(Contact contact) throws Exception { + log.info("ContactRepository.create() - Creating " + contact.getFirstName() + " " + contact.getLastName()); + + // Write the contact to the database. + em.persist(contact); + + return contact; + } + + /** + *

Updates an existing Contact object in the application database with the provided Contact object.

+ * + *

{@link javax.persistence.EntityManager#merge(Object) merge(Object)} creates a new instance of your entity, + * copies the state from the supplied entity, and makes the new copy managed. The instance you pass in will not be + * managed (any changes you make will not be part of the transaction - unless you call merge again).

+ * + *

merge(Object) however must have an object with the @Id already generated.

+ * + * @param contact The Contact object to be merged with an existing Contact + * @return The Contact that has been merged + * @throws ConstraintViolationException, ValidationException, Exception + */ + Contact update(Contact contact) throws Exception { + log.info("ContactRepository.update() - Updating " + contact.getFirstName() + " " + contact.getLastName()); + + // Either update the contact or add it if it can't be found. + em.merge(contact); + + return contact; + } + + /** + *

Deletes the provided Contact object from the application database if found there

+ * + * @param contact The Contact object to be removed from the application database + * @return The Contact object that has been successfully removed from the application database; or null + * @throws Exception + */ + Contact delete(Contact contact) throws Exception { + log.info("ContactRepository.delete() - Deleting " + contact.getFirstName() + " " + contact.getLastName()); + + if (contact.getId() != null) { + /* + * The Hibernate session (aka EntityManager's persistent context) is closed and invalidated after the commit(), + * because it is bound to a transaction. The object goes into a detached status. If you open a new persistent + * context, the object isn't known as in a persistent state in this new context, so you have to merge it. + * + * Merge sees that the object has a primary key (id), so it knows it is not new and must hit the database + * to reattach it. + * + * Note, there is NO remove method which would just take a primary key (id) and a entity class as argument. + * You first need an object in a persistent state to be able to delete it. + * + * Therefore we merge first and then we can remove it. + */ + em.remove(em.merge(contact)); + + } else { + log.info("ContactRepository.delete() - No ID was found so can't Delete."); + } + + return contact; + } + +} diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestService.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestService.java new file mode 100644 index 0000000..758efc1 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestService.java @@ -0,0 +1,345 @@ +package uk.ac.newcastle.enterprisemiddleware.contact; + +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; +import org.jboss.resteasy.reactive.Cache; +import uk.ac.newcastle.enterprisemiddleware.area.InvalidAreaCodeException; +import uk.ac.newcastle.enterprisemiddleware.util.RestServiceException; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.persistence.NoResultException; +import javax.transaction.Transactional; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + + +/** + *

This class produces a RESTful service exposing the functionality of {@link ContactService}.

+ * + *

The Path annotation defines this as a REST Web Service using JAX-RS.

+ * + *

By placing the Consumes and Produces annotations at the class level the methods all default to JSON. However, they + * can be overriden by adding the Consumes or Produces annotations to the individual methods.

+ * + *

It is Stateless to "inform the container that this RESTful web service should also be treated as an EJB and allow + * transaction demarcation when accessing the database." - Antonio Goncalves

+ * + *

The full path for accessing endpoints defined herein is: api/contacts/*

+ * + * @author Joshua Wilson + * @see ContactService + * @see javax.ws.rs.core.Response + */ +@Path("/contacts") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class ContactRestService { + @Inject + @Named("logger") + Logger log; + + @Inject + ContactService service; + + /** + *

Return all the Contacts. They are sorted alphabetically by name.

+ * + *

The url may optionally include query parameters specifying a Contact's name

+ * + *

Examples:

GET api/contacts?firstname=John
,
GET api/contacts?firstname=John&lastname=Smith

+ * + * @return A Response containing a list of Contacts + */ + @GET + @Operation(summary = "Fetch all Contacts", description = "Returns a JSON array of all stored Contact objects.") + public Response retrieveAllContacts(@QueryParam("firstname") String firstname, @QueryParam("lastname") String lastname) { + //Create an empty collection to contain the intersection of Contacts to be returned + List contacts; + + if(firstname == null && lastname == null) { + contacts = service.findAllOrderedByName(); + } else if(lastname == null) { + contacts = service.findAllByFirstName(firstname); + } else if(firstname == null) { + contacts = service.findAllByLastName(lastname); + } else { + contacts = service.findAllByFirstName(firstname); + contacts.retainAll(service.findAllByLastName(lastname)); + } + + return Response.ok(contacts).build(); + } + + /** + *

Search for and return a Contact identified by email address.

+ * + *

Path annotation includes very simple regex to differentiate between email addresses and Ids. + * DO NOT attempt to use this regex to validate email addresses.

+ * + * + * @param email The string parameter value provided as a Contact's email + * @return A Response containing a single Contact + */ + @GET + @Cache + @Path("/email/{email:.+[%40|@].+}") + @Operation( + summary = "Fetch a Contact by Email", + description = "Returns a JSON representation of the Contact object with the provided email." + ) + @APIResponses(value = { + @APIResponse(responseCode = "200", description ="Contact found"), + @APIResponse(responseCode = "404", description = "Contact with email not found") + }) + public Response retrieveContactsByEmail( + @Parameter(description = "Email of Contact to be fetched", required = true) + @PathParam("email") + String email) { + + Contact contact; + try { + contact = service.findByEmail(email); + } catch (NoResultException e) { + // Verify that the contact exists. Return 404, if not present. + throw new RestServiceException("No Contact with the email " + email + " was found!", Response.Status.NOT_FOUND); + } + return Response.ok(contact).build(); + } + + /** + *

Search for and return a Contact identified by id.

+ * + * @param id The long parameter value provided as a Contact's id + * @return A Response containing a single Contact + */ + @GET + @Cache + @Path("/{id:[0-9]+}") + @Operation( + summary = "Fetch a Contact by id", + description = "Returns a JSON representation of the Contact object with the provided id." + ) + @APIResponses(value = { + @APIResponse(responseCode = "200", description ="Contact found"), + @APIResponse(responseCode = "404", description = "Contact with id not found") + }) + public Response retrieveContactById( + @Parameter(description = "Id of Contact to be fetched") + @Schema(minimum = "0", required = true) + @PathParam("id") + long id) { + + Contact contact = service.findById(id); + if (contact == null) { + // Verify that the contact exists. Return 404, if not present. + throw new RestServiceException("No Contact with the id " + id + " was found!", Response.Status.NOT_FOUND); + } + log.info("findById " + id + ": found Contact = " + contact); + + return Response.ok(contact).build(); + } + + /** + *

Creates a new contact from the values provided. Performs validation and will return a JAX-RS response with + * either 201 (Resource created) or with a map of fields, and related errors.

+ * + * @param contact The Contact object, constructed automatically from JSON input, to be created via + * {@link ContactService#create(Contact)} + * @return A Response indicating the outcome of the create operation + */ + @SuppressWarnings("unused") + @POST + @Operation(description = "Add a new Contact to the database") + @APIResponses(value = { + @APIResponse(responseCode = "201", description = "Contact created successfully."), + @APIResponse(responseCode = "400", description = "Invalid Contact supplied in request body"), + @APIResponse(responseCode = "409", description = "Contact supplied in request body conflicts with an existing Contact"), + @APIResponse(responseCode = "500", description = "An unexpected error occurred whilst processing the request") + }) + @Transactional + public Response createContact( + @Parameter(description = "JSON representation of Contact object to be added to the database", required = true) + Contact contact) { + + if (contact == null) { + throw new RestServiceException("Bad Request", Response.Status.BAD_REQUEST); + } + + Response.ResponseBuilder builder; + + try { + // Clear the ID if accidentally set + contact.setId(null); + + // Go add the new Contact. + service.create(contact); + + // Create a "Resource Created" 201 Response and pass the contact back in case it is needed. + builder = Response.status(Response.Status.CREATED).entity(contact); + + + } catch (ConstraintViolationException ce) { + //Handle bean validation issues + Map responseObj = new HashMap<>(); + + for (ConstraintViolation violation : ce.getConstraintViolations()) { + responseObj.put(violation.getPropertyPath().toString(), violation.getMessage()); + } + throw new RestServiceException("Bad Request", responseObj, Response.Status.BAD_REQUEST, ce); + + } catch (UniqueEmailException e) { + // Handle the unique constraint violation + Map responseObj = new HashMap<>(); + responseObj.put("email", "That email is already used, please use a unique email"); + throw new RestServiceException("Bad Request", responseObj, Response.Status.CONFLICT, e); + } catch (InvalidAreaCodeException e) { + Map responseObj = new HashMap<>(); + responseObj.put("area_code", "The telephone area code provided is not recognised, please provide another"); + throw new RestServiceException("Bad Request", responseObj, Response.Status.BAD_REQUEST, e); + } catch (Exception e) { + // Handle generic exceptions + throw new RestServiceException(e); + } + + log.info("createContact completed. Contact = " + contact); + return builder.build(); + } + + /** + *

Updates the contact with the ID provided in the database. Performs validation, and will return a JAX-RS response + * with either 200 (ok), or with a map of fields, and related errors.

+ * + * @param contact The Contact object, constructed automatically from JSON input, to be updated via + * {@link ContactService#update(Contact)} + * @param id The long parameter value provided as the id of the Contact to be updated + * @return A Response indicating the outcome of the create operation + */ + @PUT + @Path("/{id:[0-9]+}") + @Operation(description = "Update a Contact in the database") + @APIResponses(value = { + @APIResponse(responseCode = "200", description = "Contact updated successfully"), + @APIResponse(responseCode = "400", description = "Invalid Contact supplied in request body"), + @APIResponse(responseCode = "404", description = "Contact with id not found"), + @APIResponse(responseCode = "409", description = "Contact details supplied in request body conflict with another existing Contact"), + @APIResponse(responseCode = "500", description = "An unexpected error occurred whilst processing the request") + }) + @Transactional + public Response updateContact( + @Parameter(description= "Id of Contact to be updated", required = true) + @Schema(minimum = "0") + @PathParam("id") + long id, + @Parameter(description = "JSON representation of Contact object to be updated in the database", required = true) + Contact contact) { + + if (contact == null || contact.getId() == null) { + throw new RestServiceException("Invalid Contact supplied in request body", Response.Status.BAD_REQUEST); + } + + if (contact.getId() != null && contact.getId() != id) { + // The client attempted to update the read-only Id. This is not permitted. + Map responseObj = new HashMap<>(); + responseObj.put("id", "The Contact ID in the request body must match that of the Contact being updated"); + throw new RestServiceException("Contact details supplied in request body conflict with another Contact", + responseObj, Response.Status.CONFLICT); + } + + if (service.findById(contact.getId()) == null) { + // Verify that the contact exists. Return 404, if not present. + throw new RestServiceException("No Contact with the id " + id + " was found!", Response.Status.NOT_FOUND); + } + + Response.ResponseBuilder builder; + + try { + // Apply the changes the Contact. + service.update(contact); + + // Create an OK Response and pass the contact back in case it is needed. + builder = Response.ok(contact); + + + } catch (ConstraintViolationException ce) { + //Handle bean validation issues + Map responseObj = new HashMap<>(); + + for (ConstraintViolation violation : ce.getConstraintViolations()) { + responseObj.put(violation.getPropertyPath().toString(), violation.getMessage()); + } + throw new RestServiceException("Bad Request", responseObj, Response.Status.BAD_REQUEST, ce); + } catch (UniqueEmailException e) { + // Handle the unique constraint violation + Map responseObj = new HashMap<>(); + responseObj.put("email", "That email is already used, please use a unique email"); + throw new RestServiceException("Contact details supplied in request body conflict with another Contact", + responseObj, Response.Status.CONFLICT, e); + } catch (InvalidAreaCodeException e) { + Map responseObj = new HashMap<>(); + responseObj.put("area_code", "The telephone area code provided is not recognised, please provide another"); + throw new RestServiceException("Bad Request", responseObj, Response.Status.BAD_REQUEST, e); + } catch (Exception e) { + // Handle generic exceptions + throw new RestServiceException(e); + } + + log.info("updateContact completed. Contact = " + contact); + return builder.build(); + } + + /** + *

Deletes a contact using the ID provided. If the ID is not present then nothing can be deleted.

+ * + *

Will return a JAX-RS response with either 204 NO CONTENT or with a map of fields, and related errors.

+ * + * @param id The Long parameter value provided as the id of the Contact to be deleted + * @return A Response indicating the outcome of the delete operation + */ + @DELETE + @Path("/{id:[0-9]+}") + @Operation(description = "Delete a Contact from the database") + @APIResponses(value = { + @APIResponse(responseCode = "204", description = "The contact has been successfully deleted"), + @APIResponse(responseCode = "400", description = "Invalid Contact id supplied"), + @APIResponse(responseCode = "404", description = "Contact with id not found"), + @APIResponse(responseCode = "500", description = "An unexpected error occurred whilst processing the request") + }) + @Transactional + public Response deleteContact( + @Parameter(description = "Id of Contact to be deleted", required = true) + @Schema(minimum = "0") + @PathParam("id") + long id) { + + Response.ResponseBuilder builder; + + Contact contact = service.findById(id); + if (contact == null) { + // Verify that the contact exists. Return 404, if not present. + throw new RestServiceException("No Contact with the id " + id + " was found!", Response.Status.NOT_FOUND); + } + + try { + service.delete(contact); + + builder = Response.noContent(); + + } catch (Exception e) { + // Handle generic exceptions + throw new RestServiceException(e); + } + log.info("deleteContact completed. Contact = " + contact); + return builder.build(); + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactService.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactService.java new file mode 100644 index 0000000..ead5016 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactService.java @@ -0,0 +1,185 @@ +package uk.ac.newcastle.enterprisemiddleware.contact; + + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import uk.ac.newcastle.enterprisemiddleware.area.Area; +import uk.ac.newcastle.enterprisemiddleware.area.AreaService; +import uk.ac.newcastle.enterprisemiddleware.area.InvalidAreaCodeException; + +import javax.enterprise.context.Dependent; +import javax.inject.Inject; +import javax.inject.Named; +import javax.validation.ConstraintViolationException; +import javax.ws.rs.ClientErrorException; +import javax.ws.rs.core.Response; +import java.util.List; +import java.util.logging.Logger; + +/** + *

This Service assumes the Control responsibility in the ECB pattern.

+ * + *

The validation is done here so that it may be used by other Boundary Resources. Other Business Logic would go here + * as well.

+ * + *

There are no access modifiers on the methods, making them 'package' scope. They should only be accessed by a + * Boundary / Web Service class with public methods.

+ * + * + * @author Joshua Wilson + * @see ContactValidator + * @see ContactRepository + */ +@Dependent +public class ContactService { + + @Inject + @Named("logger") + Logger log; + + @Inject + ContactValidator validator; + + @Inject + ContactRepository crud; + + //Removed temporarily due to non-existing AreaService + @RestClient + AreaService areaService; + + /** + *

Returns a List of all persisted {@link Contact} objects, sorted alphabetically by last name.

+ * + * @return List of Contact objects + */ + List findAllOrderedByName() { + return crud.findAllOrderedByName(); + } + + /** + *

Returns a single Contact object, specified by a Long id.

+ * + * @param id The id field of the Contact to be returned + * @return The Contact with the specified id + */ + Contact findById(Long id) { + return crud.findById(id); + } + + /** + *

Returns a single Contact object, specified by a String email.

+ * + *

If there is more than one Contact with the specified email, only the first encountered will be returned.

+ * + * @param email The email field of the Contact to be returned + * @return The first Contact with the specified email + */ + Contact findByEmail(String email) { + return crud.findByEmail(email); + } + + /** + *

Returns a single Contact object, specified by a String firstName.

+ * + * @param firstName The firstName field of the Contact to be returned + * @return The first Contact with the specified firstName + */ + List findAllByFirstName(String firstName) { + return crud.findAllByFirstName(firstName); + } + + /** + *

Returns a single Contact object, specified by a String lastName.

+ * + * @param lastName The lastName field of the Contacts to be returned + * @return The Contacts with the specified lastName + */ + List findAllByLastName(String lastName) { + return crud.findAllByLastName(lastName); + } + + /** + *

Writes the provided Contact object to the application database.

+ * + *

Validates the data in the provided Contact object using a {@link ContactValidator} object.

+ * + * @param contact The Contact object to be written to the database using a {@link ContactRepository} object + * @return The Contact object that has been successfully written to the application database + * @throws ConstraintViolationException, ValidationException, Exception + */ + Contact create(Contact contact) throws Exception { + log.info("ContactService.create() - Creating " + contact.getFirstName() + " " + contact.getLastName()); + + // Check to make sure the data fits with the parameters in the Contact model and passes validation. + validator.validateContact(contact); + + + + //Create client service instance to make REST requests to upstream service + try { + //Removed temporarily due to non-existing AreaService + Area area = areaService.getAreaById(Integer.parseInt(contact.getPhoneNumber().substring(1, 4))); + contact.setState(area.getState()); + } catch (ClientErrorException e) { + if (e.getResponse().getStatusInfo() == Response.Status.NOT_FOUND) { + throw new InvalidAreaCodeException("The area code provided does not exist", e); + } else { + throw e; + } + } + + // Write the contact to the database. + return crud.create(contact); + } + + /** + *

Updates an existing Contact object in the application database with the provided Contact object.

+ * + *

Validates the data in the provided Contact object using a ContactValidator object.

+ * + * @param contact The Contact object to be passed as an update to the application database + * @return The Contact object that has been successfully updated in the application database + * @throws ConstraintViolationException, ValidationException, Exception + */ + Contact update(Contact contact) throws Exception { + log.info("ContactService.update() - Updating " + contact.getFirstName() + " " + contact.getLastName()); + + // Check to make sure the data fits with the parameters in the Contact model and passes validation. + validator.validateContact(contact); + + try { + //Removed temporarily due to non-existing AreaService + Area area = areaService.getAreaById(Integer.parseInt(contact.getPhoneNumber().substring(1, 4))); + contact.setState(area.getState()); + } catch (ClientErrorException e) { + if (e.getResponse().getStatusInfo() == Response.Status.NOT_FOUND) { + throw new InvalidAreaCodeException("The area code provided does not exist", e); + } else { + throw e; + } + } + + // Either update the contact or add it if it can't be found. + return crud.update(contact); + } + + /** + *

Deletes the provided Contact object from the application database if found there.

+ * + * @param contact The Contact object to be removed from the application database + * @return The Contact object that has been successfully removed from the application database; or null + * @throws Exception + */ + Contact delete(Contact contact) throws Exception { + log.info("delete() - Deleting " + contact.toString()); + + Contact deletedContact = null; + + if (contact.getId() != null) { + deletedContact = crud.delete(contact); + } else { + log.info("delete() - No ID was found so can't Delete."); + } + + return deletedContact; + } +} diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactValidator.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactValidator.java new file mode 100644 index 0000000..780390c --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactValidator.java @@ -0,0 +1,89 @@ +package uk.ac.newcastle.enterprisemiddleware.contact; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.persistence.NoResultException; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; +import javax.validation.Validator; +import java.util.HashSet; +import java.util.Set; + +/** + *

This class provides methods to check Contact objects against arbitrary requirements.

+ * + * @author Joshua Wilson + * @see Contact + * @see ContactRepository + * @see javax.validation.Validator + */ +@ApplicationScoped +public class ContactValidator { + @Inject + Validator validator; + + @Inject + ContactRepository crud; + + /** + *

Validates the given Contact object and throws validation exceptions based on the type of error. If the error is standard + * bean validation errors then it will throw a ConstraintValidationException with the set of the constraints violated.

+ * + * + *

If the error is caused because an existing contact with the same email is registered it throws a regular validation + * exception so that it can be interpreted separately.

+ * + * + * @param contact The Contact object to be validated + * @throws ConstraintViolationException If Bean Validation errors exist + * @throws ValidationException If contact with the same email already exists + */ + void validateContact(Contact contact) throws ConstraintViolationException, ValidationException { + // Create a bean validator and check for issues. + Set> violations = validator.validate(contact); + + if (!violations.isEmpty()) { + throw new ConstraintViolationException(new HashSet>(violations)); + } + + // Check the uniqueness of the email address + if (emailAlreadyExists(contact.getEmail(), contact.getId())) { + throw new UniqueEmailException("Unique Email Violation"); + } + } + + /** + *

Checks if a contact with the same email address is already registered. This is the only way to easily capture the + * "@UniqueConstraint(columnNames = "email")" constraint from the Contact class.

+ * + *

Since Update will being using an email that is already in the database we need to make sure that it is the email + * from the record being updated.

+ * + * @param email The email to check is unique + * @param id The user id to check the email against if it was found + * @return boolean which represents whether the email was found, and if so if it belongs to the user with id + */ + boolean emailAlreadyExists(String email, Long id) { + Contact contact = null; + Contact contactWithID = null; + try { + contact = crud.findByEmail(email); + } catch (NoResultException e) { + // ignore + } + + if (contact != null && id != null) { + try { + contactWithID = crud.findById(id); + if (contactWithID != null && contactWithID.getEmail().equals(email)) { + contact = null; + } + } catch (NoResultException e) { + // ignore + } + } + return contact != null; + } +} + diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/UniqueEmailException.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/UniqueEmailException.java new file mode 100644 index 0000000..0989aa7 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/contact/UniqueEmailException.java @@ -0,0 +1,27 @@ +package uk.ac.newcastle.enterprisemiddleware.contact; + +import javax.validation.ValidationException; + +/** + *

ValidationException caused if a Contact's email address conflicts with that of another Contact.

+ * + *

This violates the uniqueness constraint.

+ * + * @author hugofirth + * @see Contact + */ +public class UniqueEmailException extends ValidationException { + + public UniqueEmailException(String message) { + super(message); + } + + public UniqueEmailException(String message, Throwable cause) { + super(message, cause); + } + + public UniqueEmailException(Throwable cause) { + super(cause); + } +} + diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/ErrorMessage.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/ErrorMessage.java new file mode 100644 index 0000000..602ec28 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/ErrorMessage.java @@ -0,0 +1,36 @@ +package uk.ac.newcastle.enterprisemiddleware.util; + + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import java.util.HashMap; +import java.util.Map; + +/** + *

A simple POJO to hold the details of an actual error that will be marshaled into JSON by jackson.

+ * + * @author hugofirth + */ +@RegisterForReflection +public class ErrorMessage { + private final String error; + private final Map reasons; + + public ErrorMessage(String error) { + this.error = error; + this.reasons = new HashMap<>(); + } + + public ErrorMessage(String error, Map reasons) { + this.error = error; + this.reasons = reasons; + } + + public Map getReasons() { + return reasons; + } + + public String getError() { + return error; + } +} \ No newline at end of file diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/Resources.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/Resources.java new file mode 100644 index 0000000..32a86a4 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/Resources.java @@ -0,0 +1,39 @@ +package uk.ac.newcastle.enterprisemiddleware.util; + +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.InjectionPoint; +import javax.inject.Named; +import java.util.logging.Logger; + +/** + * This class uses CDI to alias Java EE resources, such as the persistence context, to CDI beans + * + *

+ * Example injection on a managed bean field: + *

+ * + *
+ * @Inject
+ * private EntityManager em;
+ * 
+ */ +public class Resources { +// +// @Produces +// @PersistenceContext(unitName = "contacts_pu") +// private EntityManager em; + + @Produces + @Named("logger") + public Logger produceLog(InjectionPoint injectionPoint) { + return Logger.getLogger(injectionPoint.getMember().getDeclaringClass().getName()); + } +// +// @Produces +// @Named("mapper") +// public ObjectMapper produceMapper() { +// return new ObjectMapper(); +// } + +} + diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceException.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceException.java new file mode 100644 index 0000000..c868df9 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceException.java @@ -0,0 +1,72 @@ +package uk.ac.newcastle.enterprisemiddleware.util; + +import javax.ws.rs.core.Response; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +public class RestServiceException extends RuntimeException implements + Serializable { + + private static final long serialVersionUID = 1264443812161L; + private static final String defaultMsg = "An unexpected error occurred whilst processing the request"; + + private final Map reasons; + private final Response.Status status; + + public RestServiceException() { + super(defaultMsg); + this.reasons = new HashMap<>(); + this.status = Response.Status.INTERNAL_SERVER_ERROR; + } + + public RestServiceException(String msg) { + super(msg); + this.reasons = new HashMap<>(); + this.status = Response.Status.INTERNAL_SERVER_ERROR; + } + + public RestServiceException(String msg, Response.Status status) { + super(msg); + this.reasons = new HashMap<>(); + this.status = status; + } + + public RestServiceException(String msg, Map reasons, Response.Status status) { + super(msg); + this.reasons = reasons; + this.status = status; + } + + public RestServiceException(Exception e) { + super(defaultMsg, e); + this.reasons = new HashMap<>(); + this.status = Response.Status.INTERNAL_SERVER_ERROR; + } + + public RestServiceException(String msg, Exception e) { + super(msg, e); + this.reasons = new HashMap<>(); + this.status = Response.Status.INTERNAL_SERVER_ERROR; + } + + public RestServiceException(String msg, Response.Status status, Exception e) { + super(msg, e); + this.reasons = new HashMap<>(); + this.status = status; + } + + public RestServiceException(String msg, Map reasons, Response.Status status, Exception e) { + super(msg, e); + this.reasons = reasons; + this.status = status; + } + + public Map getReasons() { + return reasons; + } + + public Response.Status getStatus() { + return status; + } +} diff --git a/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceExceptionMapper.java b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceExceptionMapper.java new file mode 100644 index 0000000..f262ff2 --- /dev/null +++ b/src/main/java/uk/ac/newcastle/enterprisemiddleware/util/RestServiceExceptionMapper.java @@ -0,0 +1,37 @@ +package uk.ac.newcastle.enterprisemiddleware.util; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.logging.Logger; + +/** + *

Handler object to convert {@link RestServiceException} exception into an actual {@link Response} containing JSON + * so we can get a nice friendly error message, easily parsable by our API clients.

+ * + * @author hugofirth + */ +@Provider +public class RestServiceExceptionMapper implements ExceptionMapper { + + @Inject + @Named("logger") + Logger log; + + @Context + HttpHeaders headers; + + @Override + public Response toResponse(final RestServiceException e) { + + log.severe("Mapping RestServiceException with status + \"" + e.getStatus() + "\", message: \"" + e.getMessage() + + "\" and stack trace:" + System.getProperty("line.separator") + e); + + Response.ResponseBuilder builder = Response.status(e.getStatus()).entity(new ErrorMessage(e.getMessage(), e.getReasons())); + return builder.build(); + } +} \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/index.html b/src/main/resources/META-INF/resources/index.html new file mode 100644 index 0000000..061db10 --- /dev/null +++ b/src/main/resources/META-INF/resources/index.html @@ -0,0 +1,289 @@ + + + + + csc8104 - 1.0.0-SNAPSHOT + + + +
+
+
+ + + + + quarkus_logo_horizontal_rgb_1280px_reverse + + + + + + + + + + + + + + + + + + +
+
+
+ +
+
+
+

You just made a Quarkus application.

+

This page is served by Quarkus.

+ Visit the Dev UI +

This page: src/main/resources/META-INF/resources/index.html

+

App configuration: src/main/resources/application.properties

+

Static assets: src/main/resources/META-INF/resources/

+

Code: src/main/java

+

Generated starter code:

+
    +
  • + RESTEasy Reactive Easily start your Reactive RESTful Web Services +
    @Path: /hello +
    Related guide +
  • + +
+
+
+

Selected extensions

+
    +
  • RESTEasy Reactive Jackson
  • +
  • Hibernate ORM (guide)
  • +
  • JDBC Driver - H2
  • +
  • Narayana JTA - Transaction manager (guide)
  • +
  • Hibernate Validator (guide)
  • +
  • SmallRye OpenAPI (guide)
  • +
  • REST Client Reactive Jackson
  • +
+
Documentation
+

Practical step-by-step guides to help you achieve a specific goal. Use them to help get your work + done.

+
Set up your IDE
+

Everyone has a favorite IDE they like to use to code. Learn how to configure yours to maximize your + Quarkus productivity.

+
+
+
+ + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..478c6d2 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,15 @@ +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.rest-client.area-api.url=http://3.129.86.185:80/ +quarkus.rest-client.area-api.scope=javax.inject.Singleton # + +quarkus.swagger-ui.enable=true +quarkus.swagger-ui.always-include=true + +quarkus.http.test-port=0 +quarkus.http.test-ssl-port=0 + +quarkus.datasource.db-kind=h2 +quarkus.datasource.jdbc.url=jdbc:h2:tcp://localhost/mem:quarkus;DB_CLOSE_ON_EXIT=FALSE +quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.log.sql=true diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql new file mode 100644 index 0000000..fa2ea10 --- /dev/null +++ b/src/main/resources/import.sql @@ -0,0 +1,5 @@ +-- This file allow to write SQL commands that will be emitted in test and dev. +-- The commands are commented as their support depends of the database +-- insert into myentity (id, field) values(nextval('hibernate_sequence'), 'field-1'); +-- insert into myentity (id, field) values(nextval('hibernate_sequence'), 'field-2'); +-- insert into myentity (id, field) values(nextval('hibernate_sequence'), 'field-3'); \ No newline at end of file diff --git a/src/test/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestServiceIntegrationTest.java b/src/test/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestServiceIntegrationTest.java new file mode 100644 index 0000000..b6a7837 --- /dev/null +++ b/src/test/java/uk/ac/newcastle/enterprisemiddleware/contact/ContactRestServiceIntegrationTest.java @@ -0,0 +1,100 @@ +package uk.ac.newcastle.enterprisemiddleware.contact; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.h2.H2DatabaseTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.http.ContentType; +import io.restassured.response.Response; +import org.junit.jupiter.api.*; + +import java.util.Calendar; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@QuarkusTest +@TestHTTPEndpoint(ContactRestService.class) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +@QuarkusTestResource(H2DatabaseTestResource.class) +class ContactRestServiceIntegrationTest { + + private static Contact contact; + + @BeforeAll + static void setup() { + contact = new Contact(); + contact.setFirstName("Test"); + contact.setLastName("Account"); + contact.setEmail("test@email.com"); + contact.setBirthDate(Calendar.getInstance().getTime()); + contact.setPhoneNumber("(201) 123-4567"); + contact.setState("New Jersey"); + } + + @Test + @Order(1) + public void testCanCreateContact() { + given(). + contentType(ContentType.JSON). + body(contact). + when() + .post(). + then(). + statusCode(201); + } + + @Test + @Order(2) + public void testCanGetContacts() { + Response response = when(). + get(). + then(). + statusCode(200). + extract().response(); + + Contact[] result = response.body().as(Contact[].class); + + System.out.println(result[0]); + + assertEquals(1, result.length); + assertTrue(contact.getFirstName().equals(result[0].getFirstName()), "First name not equal"); + assertTrue(contact.getLastName().equals(result[0].getLastName()), "Last name not equal"); + assertTrue(contact.getEmail().equals(result[0].getEmail()), "Email not equal"); + assertTrue(contact.getState().equals(result[0].getState()), "State not equal Expected " + contact.getState() + " Got " + result[0].getState()); + assertTrue(contact.getPhoneNumber().equals(result[0].getPhoneNumber()), "Phone number not equal"); + } + + @Test + @Order(3) + public void testDuplicateEmailCausesError() { + given(). + contentType(ContentType.JSON). + body(contact). + when(). + post(). + then(). + statusCode(409). + body("reasons.email", containsString("email is already used")); + } + + @Test + @Order(4) + public void testCanDeleteContact() { + Response response = when(). + get(). + then(). + statusCode(200). + extract().response(); + + Contact[] result = response.body().as(Contact[].class); + + when(). + delete(result[0].getId().toString()). + then(). + statusCode(204); + } +} \ No newline at end of file