Add Auction House; Extend uniform HTTP API for TAPAS-Tasks

This commit is contained in:
Andrei Ciortea 2021-10-18 01:19:42 +02:00
parent ebb91a6010
commit 34fb3c682f
87 changed files with 3532 additions and 106 deletions

View File

@ -29,6 +29,10 @@ jobs:
run: mvn -f tapas-tasks/pom.xml --batch-mode --update-snapshots verify run: mvn -f tapas-tasks/pom.xml --batch-mode --update-snapshots verify
- run: cp ./tapas-tasks/target/tapas-tasks-0.0.1-SNAPSHOT.jar ./target - run: cp ./tapas-tasks/target/tapas-tasks-0.0.1-SNAPSHOT.jar ./target
- name: Build with Maven
run: mvn -f tapas-auction-house/pom.xml --batch-mode --update-snapshots verify
- run: cp ./tapas-auction-house/target/tapas-auction-house-0.0.1-SNAPSHOT.jar ./target
- name: Build with Maven - name: Build with Maven
run: mvn -f app/pom.xml --batch-mode --update-snapshots verify run: mvn -f app/pom.xml --batch-mode --update-snapshots verify
- run: cp ./app/target/app-0.1.0.jar ./target - run: cp ./app/target/app-0.1.0.jar ./target

View File

@ -41,6 +41,21 @@ services:
- "traefik.http.routers.tapas-tasks.entryPoints=web,websecure" - "traefik.http.routers.tapas-tasks.entryPoints=web,websecure"
- "traefik.http.routers.tapas-tasks.tls.certresolver=le" - "traefik.http.routers.tapas-tasks.tls.certresolver=le"
auction-house:
image: openjdk
command: "java -jar /data/tapas-auction-house-0.0.1-SNAPSHOT.jar"
restart: unless-stopped
volumes:
- ./:/data/
labels:
- "traefik.enable=true"
- "traefik.http.routers.tapas-auction-house.rule=Host(`tapas-auction-house.${PUB_IP}.nip.io`)"
- "traefik.http.routers.tapas-auction-house.service=tapas-tasks"
- "traefik.http.services.tapas-auction-house.loadbalancer.server.port=8082"
- "traefik.http.routers.tapas-auction-house.tls=true"
- "traefik.http.routers.tapas-auction-house.entryPoints=web,websecure"
- "traefik.http.routers.tapas-auction-house.tls.certresolver=le"
app: app:
image: openjdk image: openjdk
command: "java -jar /data/app-0.1.0.jar" command: "java -jar /data/app-0.1.0.jar"

View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true

33
tapas-auction-house/.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

@ -0,0 +1,117 @@
/*
* Copyright 2007-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import java.net.*;
import java.io.*;
import java.nio.channels.*;
import java.util.Properties;
public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "0.5.6";
/**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
+ 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();
}
}

Binary file not shown.

View File

@ -0,0 +1,2 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

View File

@ -0,0 +1,101 @@
# tapas-auction-house
The Auction House is the part of your TAPAS application that is largely responsible for the interactions
with the TAPAS applications developed by the other groups. More precisely, it is responsible for
launching and managing auctions and it is implemented following the Hexagonal Architecture (based on
examples from book "Get Your Hands Dirty on Clean Architecture" by Tom Hombergs).
Technologies: Spring Boot, Maven
**Note:** this repository contains an [EditorConfig](https://editorconfig.org/) file (`.editorconfig`)
with default editor settings. EditorConfig is supported out-of-the-box by the IntelliJ IDE. To help maintain
consistent code styles, we recommend to reuse this editor configuration file in all your services.
## Project Overview
This project provides a partial implementation of the Auction House. The code is documented in detail,
here we only include a summary of implemented features:
* running and managing auctions:
* each auction has a deadline by which it is open for bids
* once the deadline has passed, the auction house closes the auction and selects a random bid
* starting an auction using a command via an HTTP adapter (see sample request below)
* retrieving the list of open auctions via an HTTP adapter, i.e. auctions accepting bids (see sample
request below)
* receiving events when executors are added to the TAPAS application (both via HTTP and MQTT adapters)
* the logic for automatic placement of bids in auctions: the auction house will place a bid in every
auction for which there is at least one executor that can handle the type of task
being auctioned
* discovery of auction houses via a provided resource directory (see assignment sheet for
Exercises 5 & 6 for more details)
## Overview of Adapters
In addition to the overall skeleton of the auction house, the current partial implementation provides
several adapters to help you get started.
### HTTP Adapters
Sample HTTP request for launching an auction:
```shell
curl -i --location --request POST 'http://localhost:8083/auctions/' \
--header 'Content-Type: application/json' \
--data-raw '{
"taskUri" : "http://example.org",
"taskType" : "taskType1",
"deadline" : 10000
}'
HTTP/1.1 201
Content-Type: application/json
Content-Length: 131
Date: Sun, 17 Oct 2021 22:34:13 GMT
{
"auctionId":"1",
"auctionHouseUri":"http://localhost:8083/",
"taskUri":"http://example.org",
"taskType":"taskType1",
"deadline":10000
}
```
Sample HTTP request for retrieving auctions currently open for bids:
```shell
curl -i --location --request GET 'http://localhost:8083/auctions/'
HTTP/1.1 200
Content-Type: application/json
Content-Length: 133
Date: Sun, 17 Oct 2021 22:34:20 GMT
[
{
"auctionId":"1",
"auctionHouseUri":"http://localhost:8083/",
"taskUri":"http://example.org",
"taskType":"taskType1",
"deadline":10000
}
]
```
Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java)
via an HTTP request:
```shell
curl -i --location --request POST 'http://localhost:8083/executors/taskType1/executor1'
HTTP/1.1 204
Date: Sun, 17 Oct 2021 22:38:45 GMT
```
### MQTT Adapters
Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java)
via an MQTT message via HiveMQ's [MQTT CLI](https://hivemq.github.io/mqtt-cli/):
```shell
mqtt pub -t ch/unisg/tapas-group1/executors -m '{ "taskType" : "taskType1", "executorId" : "executor1" }'
```

310
tapas-auction-house/mvnw vendored Executable file
View File

@ -0,0 +1,310 @@
#!/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 /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="`which java`"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=`cd "$wdir/.."; pwd`
fi
# end of workaround
done
echo "${basedir}"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
echo "$(tr -s '\n' ' ' < "$1")"
fi
}
BASE_DIR=`find_maven_basedir "$(pwd)"`
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
if [ "$MVNW_VERBOSE" = true ]; then
echo "Found .mvn/wrapper/maven-wrapper.jar"
fi
else
if [ "$MVNW_VERBOSE" = true ]; then
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi
if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
else
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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"
else
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$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 \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

182
tapas-auction-house/mvnw.cmd vendored Normal file
View File

@ -0,0 +1,182 @@
@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 "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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 "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%" == "on" pause
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
exit /B %ERROR_CODE%

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>ch.unisg</groupId>
<artifactId>tapas-auction-house</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tapas-auction-house</name>
<description>TAPAS Auction House</description>
<properties>
<java.version>11</java.version>
</properties>
<repositories>
<repository>
<id>Eclipse Paho Repo</id>
<url>https://repo.eclipse.org/content/repositories/paho-releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>javax.transaction</groupId>
<artifactId>javax.transaction-api</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>1.1.0.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,71 @@
package ch.unisg.tapas;
import ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient;
import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher;
import ch.unisg.tapas.auctionhouse.adapter.common.clients.WebSubSubscriber;
import ch.unisg.tapas.common.AuctionHouseResourceDirectory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.net.URI;
import java.util.List;
/**
* Main TAPAS Auction House application.
*/
@SpringBootApplication
public class TapasAuctionHouseApplication {
private static final Logger LOGGER = LogManager.getLogger(TapasAuctionHouseApplication.class);
public static String RESOURCE_DIRECTORY = "https://api.interactions.ics.unisg.ch/auction-houses/";
public static String MQTT_BROKER = "tcp://broker.hivemq.com:1883";
public static void main(String[] args) {
SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class);
// We will use these bootstrap methods in Week 6:
// bootstrapMarketplaceWithWebSub();
// bootstrapMarketplaceWithMqtt();
tapasAuctioneerApp.run(args);
}
/**
* Discovers auction houses and subscribes to WebSub notifications
*/
private static void bootstrapMarketplaceWithWebSub() {
List<String> auctionHouseEndpoints = discoverAuctionHouseEndpoints();
LOGGER.info("Found auction house endpoints: " + auctionHouseEndpoints);
WebSubSubscriber subscriber = new WebSubSubscriber();
for (String endpoint : auctionHouseEndpoints) {
subscriber.subscribeToAuctionHouseEndpoint(URI.create(endpoint));
}
}
/**
* Connects to an MQTT broker, presumably the one used by all TAPAS groups to communicate with
* one another
*/
private static void bootstrapMarketplaceWithMqtt() {
try {
AuctionEventsMqttDispatcher dispatcher = new AuctionEventsMqttDispatcher();
TapasMqttClient client = TapasMqttClient.getInstance(MQTT_BROKER, dispatcher);
client.startReceivingMessages();
} catch (MqttException e) {
LOGGER.error(e.getMessage(), e);
}
}
private static List<String> discoverAuctionHouseEndpoints() {
AuctionHouseResourceDirectory rd = new AuctionHouseResourceDirectory(
URI.create(RESOURCE_DIRECTORY)
);
return rd.retrieveAuctionHouseEndpoints();
}
}

View File

@ -0,0 +1,94 @@
package ch.unisg.tapas.auctionhouse.adapter.common.clients;
import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.eclipse.paho.client.mqttv3.*;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
/**
* MQTT client for your TAPAS application. This class is defined as a singleton, but it does not have
* to be this way. This class is only provided as an example to help you bootstrap your project.
* You are welcomed to change this class as you see fit.
*/
public class TapasMqttClient {
private static final Logger LOGGER = LogManager.getLogger(TapasMqttClient.class);
private static TapasMqttClient tapasClient = null;
private MqttClient mqttClient;
private final String mqttClientId;
private final String brokerAddress;
private final MessageReceivedCallback messageReceivedCallback;
private final AuctionEventsMqttDispatcher dispatcher;
private TapasMqttClient(String brokerAddress, AuctionEventsMqttDispatcher dispatcher) {
this.mqttClientId = UUID.randomUUID().toString();
this.brokerAddress = brokerAddress;
this.messageReceivedCallback = new MessageReceivedCallback();
this.dispatcher = dispatcher;
}
public static synchronized TapasMqttClient getInstance(String brokerAddress,
AuctionEventsMqttDispatcher dispatcher) {
if (tapasClient == null) {
tapasClient = new TapasMqttClient(brokerAddress, dispatcher);
}
return tapasClient;
}
public void startReceivingMessages() throws MqttException {
mqttClient = new org.eclipse.paho.client.mqttv3.MqttClient(brokerAddress, mqttClientId, new MemoryPersistence());
mqttClient.connect();
mqttClient.setCallback(messageReceivedCallback);
subscribeToAllTopics();
}
public void stopReceivingMessages() throws MqttException {
mqttClient.disconnect();
}
private void subscribeToAllTopics() throws MqttException {
for (String topic : dispatcher.getAllTopics()) {
subscribeToTopic(topic);
}
}
private void subscribeToTopic(String topic) throws MqttException {
mqttClient.subscribe(topic);
}
private void publishMessage(String topic, String payload) throws MqttException {
MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8));
mqttClient.publish(topic, message);
}
private class MessageReceivedCallback implements MqttCallback {
@Override
public void connectionLost(Throwable cause) { }
@Override
public void messageArrived(String topic, MqttMessage message) {
LOGGER.info("Received new MQTT message for topic " + topic + ": "
+ new String(message.getPayload()));
if (topic != null && !topic.isEmpty()) {
dispatcher.dispatchEvent(topic, message);
}
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) { }
}
}

View File

@ -0,0 +1,28 @@
package ch.unisg.tapas.auctionhouse.adapter.common.clients;
import java.net.URI;
/**
* Subscribes to the WebSub hubs of auction houses discovered at run time. This class is instantiated
* from {@link ch.unisg.tapas.TapasAuctionHouseApplication} when boostraping the TAPAS marketplace
* via WebSub.
*/
public class WebSubSubscriber {
public void subscribeToAuctionHouseEndpoint(URI endpoint) {
// TODO Subscribe to the auction house endpoint via WebSub:
// 1. Send a request to the auction house in order to discover the WebSub hub to subscribe to.
// The request URI should depend on the design of the Auction House HTTP API.
// 2. Send a subscription request to the discovered WebSub hub to subscribe to events relevant
// for this auction house.
// 3. Handle the validation of intent from the WebSub hub (see WebSub protocol).
//
// Once the subscription is activated, the hub will send "fat pings" with content updates.
// The content received from the hub will depend primarily on the design of the Auction House
// HTTP API.
//
// For further details see:
// - W3C WebSub Recommendation: https://www.w3.org/TR/websub/
// - the implementation notes of the WebSub hub you are using to distribute events
}
}

View File

@ -0,0 +1,60 @@
package ch.unisg.tapas.auctionhouse.adapter.common.formats;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
/**
* Used to expose a representation of the state of an auction through an interface. This class is
* only meant as a starting point when defining a uniform HTTP API for the Auction House: feel free
* to modify this class as you see fit!
*/
public class AuctionJsonRepresentation {
public static final String MEDIA_TYPE = "application/json";
@Getter @Setter
private String auctionId;
@Getter @Setter
private String auctionHouseUri;
@Getter @Setter
private String taskUri;
@Getter @Setter
private String taskType;
@Getter @Setter
private Integer deadline;
public AuctionJsonRepresentation() { }
public AuctionJsonRepresentation(String auctionId, String auctionHouseUri, String taskUri,
String taskType, Integer deadline) {
this.auctionId = auctionId;
this.auctionHouseUri = auctionHouseUri;
this.taskUri = taskUri;
this.taskType = taskType;
this.deadline = deadline;
}
public AuctionJsonRepresentation(Auction auction) {
this.auctionId = auction.getAuctionId().getValue();
this.auctionHouseUri = auction.getAuctionHouseUri().getValue().toString();
this.taskUri = auction.getTaskUri().getValue().toString();
this.taskType = auction.getTaskType().getValue();
this.deadline = auction.getDeadline().getValue();
}
public static String serialize(Auction auction) throws JsonProcessingException {
AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction);
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper.writeValueAsString(representation);
}
}

View File

@ -0,0 +1,35 @@
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http;
import ch.unisg.tapas.auctionhouse.application.handler.ExecutorAddedHandler;
import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
/*
THIS CLASS WILL BE PROVIDED ONLY AS A TEMPLATE; POINT OUT THE API NEEDS TO BE DEFINED
*/
@RestController
public class ExecutorAddedEventListenerHttpAdapter {
@PostMapping(path = "/executors/{taskType}/{executorId}")
public ResponseEntity<String> handleExecutorAddedEvent(@PathVariable("taskType") String taskType,
@PathVariable("executorId") String executorId) {
ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent(
new ExecutorRegistry.ExecutorIdentifier(executorId),
new Auction.AuctionedTaskType(taskType)
);
ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler();
newExecutorHandler.handleNewExecutorEvent(executorAddedEvent);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}

View File

@ -0,0 +1,16 @@
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
/**
* Template for handling an executor removed event received via an HTTP request
*/
@RestController
public class ExecutorRemovedEventListenerHttpAdapter {
// TODO: add annotations for request method, request URI, etc.
public void handleExecutorRemovedEvent(@PathVariable("executorId") String executorId) {
// TODO: implement logic
}
}

View File

@ -0,0 +1,11 @@
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt;
import org.eclipse.paho.client.mqttv3.MqttMessage;
/**
* Abstract MQTT listener for auction-related events
*/
public abstract class AuctionEventMqttListener {
public abstract boolean handleEvent(MqttMessage message);
}

View File

@ -0,0 +1,51 @@
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt;
import org.eclipse.paho.client.mqttv3.*;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
/**
* Dispatches MQTT messages for known topics to associated event listeners. Used in conjunction with
* {@link ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient}.
*
* This is where you would define MQTT topics and map them to event listeners (see
* {@link AuctionEventsMqttDispatcher#initRouter()}).
*
* This class is only provided as an example to help you bootstrap the project. You are welcomed to
* change this class as you see fit.
*/
public class AuctionEventsMqttDispatcher {
private final Map<String, AuctionEventMqttListener> router;
public AuctionEventsMqttDispatcher() {
this.router = new Hashtable<>();
initRouter();
}
// TODO: Register here your topics and event listener adapters
private void initRouter() {
router.put("ch/unisg/tapas-group-tutors/executors", new ExecutorAddedEventListenerMqttAdapter());
}
/**
* Returns all topics registered with this dispatcher.
*
* @return the set of registered topics
*/
public Set<String> getAllTopics() {
return router.keySet();
}
/**
* Dispatches an event received via MQTT for a given topic.
*
* @param topic the topic for which the MQTT message was received
* @param message the received MQTT message
*/
public void dispatchEvent(String topic, MqttMessage message) {
AuctionEventMqttListener listener = router.get(topic);
listener.handleEvent(message);
}
}

View File

@ -0,0 +1,46 @@
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt;
import ch.unisg.tapas.auctionhouse.application.handler.ExecutorAddedHandler;
import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.paho.client.mqttv3.MqttMessage;
/**
* Listener that handles events when an executor was added to this TAPAS application.
*
* This class is only provided as an example to help you bootstrap the project.
*/
public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListener {
@Override
public boolean handleEvent(MqttMessage message) {
String payload = new String(message.getPayload());
try {
// Note: this messge representation is provided only as an example. You should use a
// representation that makes sense in the context of your application.
JsonNode data = new ObjectMapper().readTree(payload);
String taskType = data.get("taskType").asText();
String executorId = data.get("executorId").asText();
ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent(
new ExecutorRegistry.ExecutorIdentifier(executorId),
new Auction.AuctionedTaskType(taskType)
);
ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler();
newExecutorHandler.handleNewExecutorEvent(executorAddedEvent);
} catch (JsonProcessingException | NullPointerException e) {
// TODO: refactor logging
e.printStackTrace();
return false;
}
return true;
}
}

View File

@ -0,0 +1,18 @@
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub;
import ch.unisg.tapas.auctionhouse.application.handler.AuctionStartedHandler;
import org.springframework.web.bind.annotation.*;
/**
* This class is a template for handling auction started events received via WebSub
*/
@RestController
public class AuctionStartedEventListenerWebSubAdapter {
private final AuctionStartedHandler auctionStartedHandler;
public AuctionStartedEventListenerWebSubAdapter(AuctionStartedHandler auctionStartedHandler) {
this.auctionStartedHandler = auctionStartedHandler;
}
//TODO
}

View File

@ -0,0 +1,72 @@
package ch.unisg.tapas.auctionhouse.adapter.in.web;
import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation;
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand;
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.net.URI;
/**
* Controller that handles HTTP requests for launching auctions. This controller implements the
* {@link LaunchAuctionUseCase} use case using the {@link LaunchAuctionCommand}.
*/
@RestController
public class LaunchAuctionWebController {
private final LaunchAuctionUseCase launchAuctionUseCase;
/**
* Constructs the controller.
*
* @param launchAuctionUseCase an implementation of the launch auction use case
*/
public LaunchAuctionWebController(LaunchAuctionUseCase launchAuctionUseCase) {
this.launchAuctionUseCase = launchAuctionUseCase;
}
/**
* Handles HTTP POST requests for launching auctions. Note: you are free to modify this handler
* as you see fit to reflect the discussions for the uniform HTTP API for the auction house.
* You should also ensure that this handler has the exact behavior you would expect from the
* defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.)
*
* @param payload a representation of the auction to be launched
* @return
*/
@PostMapping(path = "/auctions/", consumes = AuctionJsonRepresentation.MEDIA_TYPE)
public ResponseEntity<String> launchAuction(@RequestBody AuctionJsonRepresentation payload) {
Auction.AuctionDeadline deadline = (payload.getDeadline() == null) ?
null : new Auction.AuctionDeadline(payload.getDeadline());
LaunchAuctionCommand command = new LaunchAuctionCommand(
new Auction.AuctionedTaskUri(URI.create(payload.getTaskUri())),
new Auction.AuctionedTaskType(payload.getTaskType()),
deadline
);
// This command returns the created auction. We need the created auction to be able to
// include a representation of it in the HTTP response.
Auction auction = launchAuctionUseCase.launchAuction(command);
try {
AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction);
String auctionJson = AuctionJsonRepresentation.serialize(auction);
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, AuctionJsonRepresentation.MEDIA_TYPE);
// Return a 201 Created status code and a representation of the created auction
return new ResponseEntity<>(auctionJson, responseHeaders, HttpStatus.CREATED);
} catch (JsonProcessingException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@ -0,0 +1,62 @@
package ch.unisg.tapas.auctionhouse.adapter.in.web;
import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation;
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery;
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
/**
* Controller that handles HTTP requests for retrieving auctions hosted by this auction house that
* are open for bids. This controller implements the {@link RetrieveOpenAuctionsUseCase} use case
* using the {@link RetrieveOpenAuctionsQuery}.
*/
@RestController
public class RetrieveOpenAuctionsWebController {
private final RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase;
public RetrieveOpenAuctionsWebController(RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase) {
this.retrieveAuctionListUseCase = retrieveAuctionListUseCase;
}
/**
* Handles HTTP GET requests to retrieve the auctions that are open. Note: you are free to modify
* this handler as you see fit to reflect the discussions for the uniform HTTP API for the
* auction house. You should also ensure that this handler has the exact behavior you would expect
* from the defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.).
*
* @return a representation of a collection with the auctions that are open for bids
*/
@GetMapping(path = "/auctions/")
public ResponseEntity<String> retrieveOpenAuctions() {
Collection<Auction> auctions =
retrieveAuctionListUseCase.retrieveAuctions(new RetrieveOpenAuctionsQuery());
ObjectMapper mapper = new ObjectMapper();
ArrayNode array = mapper.createArrayNode();
for (Auction auction : auctions) {
AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction);
JsonNode node = mapper.valueToTree(representation);
array.add(node);
}
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json");
// TODO before providing to students: remove hub links
responseHeaders.add(HttpHeaders.LINK, "<https://pubsubhubbub.appspot.com/>; rel=\"hub\"");
responseHeaders.add(HttpHeaders.LINK, "<http://example.org/auctions/>; rel=\"self\"");
return new ResponseEntity<>(array.toString(), responseHeaders, HttpStatus.OK);
}
}

View File

@ -0,0 +1,37 @@
package ch.unisg.tapas.auctionhouse.adapter.out.messaging.websub;
import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent;
import ch.unisg.tapas.common.ConfigProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* This class is a template for publishing auction started events via WebSub.
*/
@Component
@Primary
public class PublishAuctionStartedEventWebSubAdapter implements AuctionStartedEventPort {
// You can use this object to retrieve properties from application.properties, e.g. the
// WebSub hub publish endpoint, etc.
@Autowired
private ConfigProperties config;
@Override
public void publishAuctionStartedEvent(AuctionStartedEvent event) {
// TODO
}
}

View File

@ -0,0 +1,20 @@
package ch.unisg.tapas.auctionhouse.adapter.out.web;
import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort;
import ch.unisg.tapas.auctionhouse.domain.AuctionWonEvent;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
/**
* This class is a template for sending auction won events via HTTP. This class was created here only
* as a placeholder, it is up to you to decide how such events should be sent (e.g., via HTTP,
* WebSub, etc.).
*/
@Component
@Primary
public class AuctionWonEventHttpAdapter implements AuctionWonEventPort {
@Override
public void publishAuctionWonEvent(AuctionWonEvent event) {
}
}

View File

@ -0,0 +1,19 @@
package ch.unisg.tapas.auctionhouse.adapter.out.web;
import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommand;
import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommandPort;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
/**
* This class is a tempalte for implementing a place bid for auction command via HTTP.
*/
@Component
@Primary
public class PlaceBidForAuctionCommandHttpAdapter implements PlaceBidForAuctionCommandPort {
@Override
public void placeBid(PlaceBidForAuctionCommand command) {
// TODO
}
}

View File

@ -0,0 +1,59 @@
package ch.unisg.tapas.auctionhouse.application.handler;
import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEvent;
import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEventHandler;
import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommand;
import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommandPort;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.auctionhouse.domain.Bid;
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry;
import ch.unisg.tapas.common.ConfigProperties;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Handler for auction started events. This handler will automatically bid in any auction for a
* task of known type, i.e. a task for which the auction house knows an executor is available.
*/
@Component
public class AuctionStartedHandler implements AuctionStartedEventHandler {
private static final Logger LOGGER = LogManager.getLogger(AuctionStartedHandler.class);
@Autowired
private ConfigProperties config;
@Autowired
private PlaceBidForAuctionCommandPort placeBidForAuctionCommandPort;
/**
* Handles an auction started event and bids in all auctions for tasks of known types.
*
* @param auctionStartedEvent the auction started domain event
* @return true unless a runtime exception occurs
*/
@Override
public boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent) {
Auction auction = auctionStartedEvent.getAuction();
if (ExecutorRegistry.getInstance().containsTaskType(auction.getTaskType())) {
LOGGER.info("Placing bid for task " + auction.getTaskUri() + " of type "
+ auction.getTaskType() + " in auction " + auction.getAuctionId()
+ " from auction house " + auction.getAuctionHouseUri().getValue().toString());
Bid bid = new Bid(auction.getAuctionId(),
new Bid.BidderName(config.getGroupName()),
new Bid.BidderAuctionHouseUri(config.getAuctionHouseUri()),
new Bid.BidderTaskListUri(config.getTaskListUri())
);
PlaceBidForAuctionCommand command = new PlaceBidForAuctionCommand(auction, bid);
placeBidForAuctionCommandPort.placeBid(command);
} else {
LOGGER.info("Cannot execute this task type: " + auction.getTaskType().getValue());
}
return true;
}
}

View File

@ -0,0 +1,16 @@
package ch.unisg.tapas.auctionhouse.application.handler;
import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent;
import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEventHandler;
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry;
import org.springframework.stereotype.Component;
@Component
public class ExecutorAddedHandler implements ExecutorAddedEventHandler {
@Override
public boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent) {
return ExecutorRegistry.getInstance().addExecutor(executorAddedEvent.getTaskType(),
executorAddedEvent.getExecutorId());
}
}

View File

@ -0,0 +1,19 @@
package ch.unisg.tapas.auctionhouse.application.handler;
import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEvent;
import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEventHandler;
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry;
import org.springframework.stereotype.Component;
/**
* Handler for executor removed events. It removes the executor from this auction house's executor
* registry.
*/
@Component
public class ExecutorRemovedHandler implements ExecutorRemovedEventHandler {
@Override
public boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent) {
return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorId());
}
}

View File

@ -0,0 +1,21 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.common.SelfValidating;
import lombok.Value;
import javax.validation.constraints.NotNull;
/**
* Event that notifies this auction house that an auction was started by another auction house.
*/
@Value
public class AuctionStartedEvent extends SelfValidating<AuctionStartedEvent> {
@NotNull
private final Auction auction;
public AuctionStartedEvent(Auction auction) {
this.auction = auction;
this.validateSelf();
}
}

View File

@ -0,0 +1,6 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
public interface AuctionStartedEventHandler {
boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent);
}

View File

@ -0,0 +1,32 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskType;
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier;
import ch.unisg.tapas.common.SelfValidating;
import lombok.Value;
import javax.validation.constraints.NotNull;
/**
* Event that notifies the auction house that an executor has been added to this TAPAS application.
*/
@Value
public class ExecutorAddedEvent extends SelfValidating<ExecutorAddedEvent> {
@NotNull
private final ExecutorIdentifier executorId;
@NotNull
private final AuctionedTaskType taskType;
/**
* Constructs an executor added event.
*
* @param executorId the identifier of the executor that was added to this TAPAS application
*/
public ExecutorAddedEvent(ExecutorIdentifier executorId, AuctionedTaskType taskType) {
this.executorId = executorId;
this.taskType = taskType;
this.validateSelf();
}
}

View File

@ -0,0 +1,6 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
public interface ExecutorAddedEventHandler {
boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent);
}

View File

@ -0,0 +1,26 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier;
import ch.unisg.tapas.common.SelfValidating;
import lombok.Value;
import javax.validation.constraints.NotNull;
/**
* Event that notifies the auction house that an executor has been removed from this TAPAS application.
*/
@Value
public class ExecutorRemovedEvent extends SelfValidating<ExecutorRemovedEvent> {
@NotNull
private final ExecutorIdentifier executorId;
/**
* Constructs an executor removed event.
*
* @param executorId the identifier of the executor that was removed from this TAPAS application
*/
public ExecutorRemovedEvent(ExecutorIdentifier executorId) {
this.executorId = executorId;
this.validateSelf();
}
}

View File

@ -0,0 +1,6 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
public interface ExecutorRemovedEventHandler {
boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent);
}

View File

@ -0,0 +1,37 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.common.SelfValidating;
import lombok.Value;
import javax.validation.constraints.NotNull;
/**
* Command for launching an auction in this auction house.
*/
@Value
public class LaunchAuctionCommand extends SelfValidating<LaunchAuctionCommand> {
@NotNull
private final Auction.AuctionedTaskUri taskUri;
@NotNull
private final Auction.AuctionedTaskType taskType;
private final Auction.AuctionDeadline deadline;
/**
* Constructs the launch action command.
*
* @param taskUri the URI of the auctioned task
* @param taskType the type of the auctioned task
* @param deadline the deadline by which the auction should receive bids (can be null if none)
*/
public LaunchAuctionCommand(Auction.AuctionedTaskUri taskUri, Auction.AuctionedTaskType taskType,
Auction.AuctionDeadline deadline) {
this.taskUri = taskUri;
this.taskType = taskType;
this.deadline = deadline;
this.validateSelf();
}
}

View File

@ -0,0 +1,8 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
import ch.unisg.tapas.auctionhouse.domain.Auction;
public interface LaunchAuctionUseCase {
Auction launchAuction(LaunchAuctionCommand command);
}

View File

@ -0,0 +1,7 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
/**
* Query used to retrieve open auctions. Although this query is empty, we model it to convey the
* domain semantics and to reduce coupling.
*/
public class RetrieveOpenAuctionsQuery { }

View File

@ -0,0 +1,10 @@
package ch.unisg.tapas.auctionhouse.application.port.in;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import java.util.Collection;
public interface RetrieveOpenAuctionsUseCase {
Collection<Auction> retrieveAuctions(RetrieveOpenAuctionsQuery query);
}

View File

@ -0,0 +1,11 @@
package ch.unisg.tapas.auctionhouse.application.port.out;
import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent;
/**
* Port for sending out auction started events
*/
public interface AuctionStartedEventPort {
void publishAuctionStartedEvent(AuctionStartedEvent event);
}

View File

@ -0,0 +1,11 @@
package ch.unisg.tapas.auctionhouse.application.port.out;
import ch.unisg.tapas.auctionhouse.domain.AuctionWonEvent;
/**
* Port for sending out auction won events
*/
public interface AuctionWonEventPort {
void publishAuctionWonEvent(AuctionWonEvent event);
}

View File

@ -0,0 +1,25 @@
package ch.unisg.tapas.auctionhouse.application.port.out;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.auctionhouse.domain.Bid;
import ch.unisg.tapas.common.SelfValidating;
import lombok.Value;
import javax.validation.constraints.NotNull;
/**
* Command to place a bid for a given auction.
*/
@Value
public class PlaceBidForAuctionCommand extends SelfValidating<PlaceBidForAuctionCommand> {
@NotNull
private final Auction auction;
@NotNull
private final Bid bid;
public PlaceBidForAuctionCommand(Auction auction, Bid bid) {
this.auction = auction;
this.bid = bid;
this.validateSelf();
}
}

View File

@ -0,0 +1,6 @@
package ch.unisg.tapas.auctionhouse.application.port.out;
public interface PlaceBidForAuctionCommandPort {
void placeBid(PlaceBidForAuctionCommand command);
}

View File

@ -0,0 +1,22 @@
package ch.unisg.tapas.auctionhouse.application.service;
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery;
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase;
import ch.unisg.tapas.auctionhouse.domain.Auction;
import ch.unisg.tapas.auctionhouse.domain.AuctionRegistry;
import org.springframework.stereotype.Component;
import java.util.Collection;
/**
* Service that implements {@link RetrieveOpenAuctionsUseCase} to retrieve all auctions in this auction
* house that are open for bids.
*/
@Component
public class RetrieveOpenAuctionsService implements RetrieveOpenAuctionsUseCase {
@Override
public Collection<Auction> retrieveAuctions(RetrieveOpenAuctionsQuery query) {
return AuctionRegistry.getInstance().getOpenAuctions();
}
}

View File

@ -0,0 +1,113 @@
package ch.unisg.tapas.auctionhouse.application.service;
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand;
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase;
import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort;
import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort;
import ch.unisg.tapas.auctionhouse.domain.*;
import ch.unisg.tapas.common.ConfigProperties;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Service that implements the {@link LaunchAuctionUseCase} to start an auction. If a deadline is
* specified for the auction, the service automatically closes the auction at the deadline. If a
* deadline is not specified, the service closes the auction after 10s by default.
*/
@Component
public class StartAuctionService implements LaunchAuctionUseCase {
private static final Logger LOGGER = LogManager.getLogger(StartAuctionService.class);
private final static int DEFAULT_AUCTION_DEADLINE_MILLIS = 10000;
// Event port used to publish an auction started event
private final AuctionStartedEventPort auctionStartedEventPort;
// Event port used to publish an auction won event
private final AuctionWonEventPort auctionWonEventPort;
private final ScheduledExecutorService service;
private final AuctionRegistry auctions;
@Autowired
private ConfigProperties config;
public StartAuctionService(AuctionStartedEventPort auctionStartedEventPort,
AuctionWonEventPort auctionWonEventPort) {
this.auctionStartedEventPort = auctionStartedEventPort;
this.auctionWonEventPort = auctionWonEventPort;
this.auctions = AuctionRegistry.getInstance();
this.service = Executors.newScheduledThreadPool(1);
}
/**
* Launches an auction.
*
* @param command the domain command used to launch the auction (see {@link LaunchAuctionCommand})
* @return the launched auction
*/
@Override
public Auction launchAuction(LaunchAuctionCommand command) {
Auction.AuctionDeadline deadline = (command.getDeadline() == null) ?
new Auction.AuctionDeadline(DEFAULT_AUCTION_DEADLINE_MILLIS) : command.getDeadline();
// Create a new auction and add it to the auction registry
Auction auction = new Auction(new Auction.AuctionHouseUri(config.getAuctionHouseUri()),
command.getTaskUri(), command.getTaskType(), deadline);
auctions.addAuction(auction);
// Schedule the closing of the auction at the deadline
service.schedule(new CloseAuctionTask(auction.getAuctionId()), deadline.getValue(),
TimeUnit.MILLISECONDS);
// Publish an auction started event
AuctionStartedEvent auctionStartedEvent = new AuctionStartedEvent(auction);
auctionStartedEventPort.publishAuctionStartedEvent(auctionStartedEvent);
return auction;
}
/**
* This task closes the auction at the deadline and selects a winner if any bids were placed. It
* also sends out associated events and commands.
*/
private class CloseAuctionTask implements Runnable {
Auction.AuctionId auctionId;
public CloseAuctionTask(Auction.AuctionId auctionId) {
this.auctionId = auctionId;
}
@Override
public void run() {
Optional<Auction> auctionOpt = auctions.getAuctionById(auctionId);
if (auctionOpt.isPresent()) {
Auction auction = auctionOpt.get();
Optional<Bid> bid = auction.selectBid();
// Close the auction
auction.close();
if (bid.isPresent()) {
// Notify the bidder
Bid.BidderName bidderName = bid.get().getBidderName();
LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " for task "
+ auction.getTaskUri().getValue() + " won by " + bidderName.getValue());
// Send an auction won event for the winning bid
auctionWonEventPort.publishAuctionWonEvent(new AuctionWonEvent(bid.get()));
} else {
LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " ended with no bids for task "
+ auction.getTaskUri().getValue());
}
}
}
}
}

View File

@ -0,0 +1,171 @@
package ch.unisg.tapas.auctionhouse.domain;
import lombok.Getter;
import lombok.Value;
import java.net.URI;
import java.util.*;
/**
* Domain entity that models an auction.
*/
public class Auction {
// Auctions have two possible states:
// - open: waiting for bids
// - closed: the auction deadline has expired, there may or may not be a winning bid
public enum Status {
OPEN, CLOSED
}
// One way to generate auction identifiers is incremental starting from 1. This makes identifiers
// predictable, which can help with debugging when multiple parties are interacting, but it also
// means that auction identifiers are not universally unique unless they are part of a URI.
// An alternative would be to use UUIDs (see constructor).
private static long AUCTION_COUNTER = 1;
@Getter
private AuctionId auctionId;
@Getter
private AuctionStatus auctionStatus;
// URI that identifies the auction house that started this auction. Given a uniform, standard
// HTTP API for auction houses, this URI can then be used as a base URI for interacting with
// the identified auction house.
@Getter
private final AuctionHouseUri auctionHouseUri;
// URI that identifies the task for which the auction was launched. URIs are uniform identifiers
// and can be referenced independent of context: because we have defined a uniform HTTP API for
// TAPAS-Tasks, we can dereference this URI to retrieve a complete representation of the
// auctioned task.
@Getter
private final AuctionedTaskUri taskUri;
// The type of the task being auctioned. We could also retrieve the task type by dereferencing
// the task's URI, but given that the bidding is defined primarily based on task types, knowing
// the task type avoids an additional HTTP request.
@Getter
private final AuctionedTaskType taskType;
// The deadline by which bids can be placed. Once the deadline expires, the auction is closed.
@Getter
private final AuctionDeadline deadline;
// Available bids.
@Getter
private final List<Bid> bids;
/**
* Constructs an auction.
*
* @param auctionHouseUri the URI of the auction hause that started the auction
* @param taskUri the URI of the task being auctioned
* @param taskType the type of the task being auctioned
* @param deadline the deadline by which the auction is open for bids
*/
public Auction(AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri,
AuctionedTaskType taskType, AuctionDeadline deadline) {
// Generates an incremental identifier
this.auctionId = new AuctionId("" + AUCTION_COUNTER ++);
// As an alternative, we could also generate an UUID
// this.auctionId = new AuctionId(UUID.randomUUID().toString());
this.auctionStatus = new AuctionStatus(Status.OPEN);
this.auctionHouseUri = auctionHouseUri;
this.taskUri = taskUri;
this.taskType = taskType;
this.deadline = deadline;
this.bids = new ArrayList<>();
}
/**
* Constructs an auction.
*
* @param auctionId the identifier of the auction
* @param auctionHouseUri the URI of the auction hause that started the auction
* @param taskUri the URI of the task being auctioned
* @param taskType the type of the task being auctioned
* @param deadline the deadline by which the auction is open for bids
*/
public Auction(AuctionId auctionId, AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri,
AuctionedTaskType taskType, AuctionDeadline deadline) {
this(auctionHouseUri, taskUri, taskType, deadline);
this.auctionId = auctionId;
}
/**
* Places a bid for this auction.
*
* @param bid the bid
*/
public void addBid(Bid bid) {
bids.add(bid);
}
/**
* Selects a bid randomly from the bids available for this auction.
*
* @return a winning bid or Optional.empty if no bid was made in this auction.
*/
public Optional<Bid> selectBid() {
if (bids.isEmpty()) {
return Optional.empty();
}
int index = new Random().nextInt(bids.size());
return Optional.of(bids.get(index));
}
/**
* Checks if the auction is open for bids.
*
* @return true if open for bids, false if the auction is closed
*/
public boolean isOpen() {
return auctionStatus.getValue() == Status.OPEN;
}
/**
* Closes the auction. Called by the StartAuctionService after the auction deadline has expired.
*/
public void close() {
auctionStatus = new AuctionStatus(Status.CLOSED);
}
/*
* Definitions of Value Objects
*/
@Value
public static class AuctionId {
String value;
}
@Value
public static class AuctionStatus {
Status value;
}
@Value
public static class AuctionHouseUri {
URI value;
}
@Value
public static class AuctionedTaskUri {
URI value;
}
@Value
public static class AuctionedTaskType {
String value;
}
@Value
public static class AuctionDeadline {
int value;
}
}

View File

@ -0,0 +1,105 @@
package ch.unisg.tapas.auctionhouse.domain;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* Registry that keeps an in-memory history of auctions (both open for bids and closed). This class
* is a singleton. See also {@link Auction}.
*/
public class AuctionRegistry {
private static AuctionRegistry registry;
private final Map<Auction.AuctionId, Auction> auctions;
private AuctionRegistry() {
this.auctions = new Hashtable<>();
}
/**
* Retrieves a reference to the auction registry.
*
* @return the auction registry
*/
public static synchronized AuctionRegistry getInstance() {
if (registry == null) {
registry = new AuctionRegistry();
}
return registry;
}
/**
* Adds a new auction to the registry
* @param auction the new auction
*/
public void addAuction(Auction auction) {
auctions.put(auction.getAuctionId(), auction);
}
/**
* Places a bid. See also {@link Bid}.
*
* @param bid the bid to be placed.
* @return false if the bid is for an auction with an unknown identifier, true otherwise
*/
public boolean placeBid(Bid bid) {
if (!containsAuctionWithId(bid.getAuctionId())) {
return false;
}
Auction auction = getAuctionById(bid.getAuctionId()).get();
auction.addBid(bid);
auctions.put(bid.getAuctionId(), auction);
return true;
}
/**
* Checks if the registry contains an auction with the given identifier.
*
* @param auctionId the auction's identifier
* @return true if the registry contains an auction with the given identifier, false otherwise
*/
public boolean containsAuctionWithId(Auction.AuctionId auctionId) {
return auctions.containsKey(auctionId);
}
/**
* Retrieves the auction with the given identifier if it exists.
*
* @param auctionId the auction's identifier
* @return the auction or Optional.empty if the identifier is unknown
*/
public Optional<Auction> getAuctionById(Auction.AuctionId auctionId) {
if (containsAuctionWithId(auctionId)) {
return Optional.of(auctions.get(auctionId));
}
return Optional.empty();
}
/**
* Retrieves all auctions in the registry.
*
* @return a collection with all auctions
*/
public Collection<Auction> getAllAuctions() {
return auctions.values();
}
/**
* Retrieves only the auctions that are open for bids.
*
* @return a collection with all open auctions
*/
public Collection<Auction> getOpenAuctions() {
return getAllAuctions()
.stream()
.filter(auction -> auction.isOpen())
.collect(Collectors.toList());
}
}

View File

@ -0,0 +1,15 @@
package ch.unisg.tapas.auctionhouse.domain;
import lombok.Getter;
/**
* A domain event that models an auction has started.
*/
public class AuctionStartedEvent {
@Getter
private Auction auction;
public AuctionStartedEvent(Auction auction) {
this.auction = auction;
}
}

View File

@ -0,0 +1,16 @@
package ch.unisg.tapas.auctionhouse.domain;
import lombok.Getter;
/**
* A domain event that models an auction was won.
*/
public class AuctionWonEvent {
// The winning bid
@Getter
private Bid winningBid;
public AuctionWonEvent(Bid winningBid) {
this.winningBid = winningBid;
}
}

View File

@ -0,0 +1,66 @@
package ch.unisg.tapas.auctionhouse.domain;
import lombok.Getter;
import lombok.Value;
import java.net.URI;
/**
* Domain entity that models a bid.
*/
public class Bid {
// The identifier of the auction for which the bid is placed
@Getter
private final Auction.AuctionId auctionId;
// The name of the bidder, i.e. the identifier of the TAPAS group
@Getter
private final BidderName bidderName;
// URI that identifies the auction house of the bidder. Given a uniform, standard HTTP API for
// auction houses, this URI can then be used as a base URI for interacting with the auction house
// of the bidder.
@Getter
private final BidderAuctionHouseUri bidderAuctionHouseUri;
// URI that identifies the TAPAS-Tasks task list of the bidder. Given a uniform, standard HTTP API
// for TAPAS-Tasks, this URI can then be used as a base URI for interacting with the list of tasks
// of the bidder, e.g. to delegate a task.
@Getter
private final BidderTaskListUri bidderTaskListUri;
/**
* Constructs a bid.
*
* @param auctionId the identifier of the auction for which the bid is placed
* @param bidderName the name of the bidder, i.e. the identifier of the TAPAS group
* @param auctionHouseUri the URI of the bidder's auction house
* @param taskListUri the URI fo the bidder's list of tasks
*/
public Bid(Auction.AuctionId auctionId, BidderName bidderName, BidderAuctionHouseUri auctionHouseUri,
BidderTaskListUri taskListUri) {
this.auctionId = auctionId;
this.bidderName = bidderName;
this.bidderAuctionHouseUri = auctionHouseUri;
this.bidderTaskListUri = taskListUri;
}
/*
* Definitions of Value Objects
*/
@Value
public static class BidderName {
private String value;
}
@Value
public static class BidderAuctionHouseUri {
private URI value;
}
@Value
public static class BidderTaskListUri {
private URI value;
}
}

View File

@ -0,0 +1,86 @@
package ch.unisg.tapas.auctionhouse.domain;
import lombok.Value;
import java.util.*;
/**
* Registry that keeps a track of executors internal to the TAPAS application and the types of tasks
* they can achieve. One executor may correspond to multiple task types. This mapping is used when
* bidding for tasks: the auction house will only bid for tasks for which there is a known executor.
* This class is a singleton.
*/
public class ExecutorRegistry {
private static ExecutorRegistry registry;
private final Map<Auction.AuctionedTaskType, Set<ExecutorIdentifier>> executors;
private ExecutorRegistry() {
this.executors = new Hashtable<>();
}
public static synchronized ExecutorRegistry getInstance() {
if (registry == null) {
registry = new ExecutorRegistry();
}
return registry;
}
/**
* Adds an executor to the registry for a given task type.
*
* @param taskType the type of the task
* @param executorIdentifier the identifier of the executor (can be any string)
* @return true unless a runtime exception occurs
*/
public boolean addExecutor(Auction.AuctionedTaskType taskType, ExecutorIdentifier executorIdentifier) {
Set<ExecutorIdentifier> taskTypeExecs = executors.getOrDefault(taskType,
Collections.synchronizedSet(new HashSet<>()));
taskTypeExecs.add(executorIdentifier);
executors.put(taskType, taskTypeExecs);
return true;
}
/**
* Removes an executor from the registry. The executor is disassociated from all known task types.
*
* @param executorIdentifier the identifier of the executor (can be any string)
* @return true unless a runtime exception occurs
*/
public boolean removeExecutor(ExecutorIdentifier executorIdentifier) {
Iterator<Auction.AuctionedTaskType> iterator = executors.keySet().iterator();
while (iterator.hasNext()) {
Auction.AuctionedTaskType taskType = iterator.next();
Set<ExecutorIdentifier> set = executors.get(taskType);
set.remove(executorIdentifier);
if (set.isEmpty()) {
iterator.remove();
}
}
return true;
}
/**
* Checks if the registry contains an executor for a given task type. Used during an auction to
* decide if a bid should be placed.
*
* @param taskType the task type being auctioned
* @return
*/
public boolean containsTaskType(Auction.AuctionedTaskType taskType) {
return executors.containsKey(taskType);
}
// Value Object for the executor identifier
@Value
public static class ExecutorIdentifier {
String value;
}
}

View File

@ -0,0 +1,57 @@
package ch.unisg.tapas.common;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.ArrayList;
import java.util.List;
/**
* Class that wraps up the resource directory used to discover auction houses in Week 6.
*/
public class AuctionHouseResourceDirectory {
private final URI rdEndpoint;
/**
* Constructs a resource directory for auction house given a known URI.
*
* @param rdEndpoint the based endpoint of the resource directory
*/
public AuctionHouseResourceDirectory(URI rdEndpoint) {
this.rdEndpoint = rdEndpoint;
}
/**
* Retrieves the endpoints of all auctions houses registered with this directory.
* @return
*/
public List<String> retrieveAuctionHouseEndpoints() {
List<String> auctionHouseEndpoints = new ArrayList<>();
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(rdEndpoint).GET().build();
HttpResponse<String> response = HttpClient.newBuilder().build()
.send(request, HttpResponse.BodyHandlers.ofString());
// For simplicity, here we just hard code the current representation used by our
// resource directory for auction houses
ObjectMapper objectMapper = new ObjectMapper();
JsonNode payload = objectMapper.readTree(response.body());
for (JsonNode node : payload) {
auctionHouseEndpoints.add(node.get("endpoint").asText());
}
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
return auctionHouseEndpoints;
}
}

View File

@ -0,0 +1,64 @@
package ch.unisg.tapas.common;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.net.URI;
/**
* Used to access properties provided via application.properties
*/
@Component
public class ConfigProperties {
@Autowired
private Environment environment;
/**
* Retrieves the URI of the WebSub hub. In this project, we use a single WebSub hub, but we could
* use multiple.
*
* @return the URI of the WebSub hub
*/
public URI getWebSubHub() {
return URI.create(environment.getProperty("websub.hub"));
}
/**
* Retrieves the URI used to publish content via WebSub. In this project, we use a single
* WebSub hub, but we could use multiple. This URI is usually different from the WebSub hub URI.
*
* @return URI used to publish content via the WebSub hub
*/
public URI getWebSubPublishEndpoint() {
return URI.create(environment.getProperty("websub.hub.publish"));
}
/**
* Retrieves the name of the group providing this auction house.
*
* @return the identifier of the group, e.g. tapas-group1
*/
public String getGroupName() {
return environment.getProperty("group");
}
/**
* Retrieves the base URI of this auction house.
*
* @return the base URI of this auction house
*/
public URI getAuctionHouseUri() {
return URI.create(environment.getProperty("auction.house.uri"));
}
/**
* Retrieves the URI of the TAPAS-Tasks task list of this TAPAS applicatoin. This is used, e.g.,
* when placing a bid during the auction (see also {@link ch.unisg.tapas.auctionhouse.domain.Bid}).
*
* @return
*/
public URI getTaskListUri() {
return URI.create(environment.getProperty("tasks.list.uri"));
}
}

View File

@ -0,0 +1,25 @@
package ch.unisg.tapas.common;
import javax.validation.*;
import java.util.Set;
public class SelfValidating<T> {
private Validator validator;
public SelfValidating() {
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
/**
* Evaluates all Bean Validations on the attributes of this
* instance.
*/
protected void validateSelf() {
Set<ConstraintViolation<T>> violations = validator.validate((T) this);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
}

View File

@ -0,0 +1,8 @@
server.port=8082
websub.hub=https://websub.appspot.com/
websub.hub.publish=https://websub.appspot.com/
group=tapas-group-tutors
auction.house.uri=https://tapas-auction-house.86-119-34-23.nip.io/
tasks.list.uri=https://tapas-tasks.86-119-34-23.nip.io/

View File

@ -0,0 +1,13 @@
package ch.unisg.tapas;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class TapasAuctionHouseApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -11,61 +11,159 @@ with default editor settings. EditorConfig is supported out-of-the-box by the In
consistent code styles, we recommend to reuse this editor configuration file in all your services. consistent code styles, we recommend to reuse this editor configuration file in all your services.
## HTTP API Overview ## HTTP API Overview
The code we provide includes a minimalistic HTTP API for (i) creating a new task and (ii) retrieving The code we provide includes a minimalistic uniform HTTP API for (i) creating a new task, (ii) retrieving
the representation of a task. a representation of the current state of a task, and (iii) patching the representation of a task, which
is mapped to a domain/integration event.
The representations exchanged with the API use two media types:
* a JSON-based format for task with the media type `application/task+json`; this media type is defined
in the context of our project, but could be [registered with IANA](https://www.iana.org/assignments/media-types)
to promote interoperability (see
[TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java)
for more details)
* the [JSON Patch](http://jsonpatch.com/) format with the registered media type `application/json-patch+json`, which is also a
JSON-based format (see sample HTTP requests below).
For further developing and working with your HTTP API, we recommend to use [Postman](https://www.postman.com/). For further developing and working with your HTTP API, we recommend to use [Postman](https://www.postman.com/).
### Creating a new task ### Creating a new task
A new task is created via an `HTTP POST` request to the `/tasks/` endpoint. The body of the request A new task is created via an `HTTP POST` request to the `/tasks/` endpoint. The body of the request
must include a JSON payload with the content type `application/json` and two required fields: must include a representation of the task to be created using the content type `application/task+json`
defined in the context of this project. A valid representation must include at least two required fields
(see [TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java)
for more details):
* `taskName`: a string that represents the name of the task to be created * `taskName`: a string that represents the name of the task to be created
* `taskType`: a string that represents the type of the task to be created * `taskType`: a string that represents the type of the task to be created
A sample HTTP request with `curl`: A sample HTTP request with `curl`:
```shell ```shell
curl -i --location --request POST 'http://localhost:8081/tasks/' --header 'Content-Type: application/json' --data-raw '{ curl -i --location --request POST 'http://localhost:8081/tasks/' \
--header 'Content-Type: application/task+json' \
--data-raw '{
"taskName" : "task1", "taskName" : "task1",
"taskType" : "type1" "taskType" : "computation",
"originalTaskUri" : "http://example.org",
"inputData" : "1+1"
}' }'
HTTP/1.1 201 HTTP/1.1 201
Content-Type: application/json Location: http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354
Content-Length: 142 Content-Type: application/task+json
Date: Sun, 03 Oct 2021 17:25:32 GMT Content-Length: 170
Date: Sun, 17 Oct 2021 21:03:34 GMT
{ {
"taskType" : "type1", "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskState" : "OPEN",
"taskListName" : "tapas-tasks-tutors",
"taskName":"task1", "taskName":"task1",
"taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" "taskType":"computation",
"taskStatus":"OPEN",
"originalTaskUri":"http://example.org",
"inputData":"1+1"
} }
``` ```
If the task is created successfuly, a `201 Created` status code is returned together with a JSON If the task is created successfuly, a `201 Created` status code is returned together with a
representation of the created task. The representation includes, among others, a _universally unique representation of the created task. The response also includes a `Location` header filed that points
identifier (UUID)_ for the newly created task (`taskId`). to the URI of the newly created task.
### Retrieving a task ### Retrieving a task
The representation of a task is retrieved via an `HTTP GET` request to the `/tasks/<task-UUID>` endpoint. The representation of a task is retrieved via an `HTTP GET` request to the URI of task.
A sample HTTP request with `curl`: A sample HTTP request with `curl`:
```shell ```shell
curl -i --location --request GET 'http://localhost:8081/tasks/53cb19d6-2d9b-486f-98c7-c96c93b037f0' curl -i --location --request GET 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354'
HTTP/1.1 200 HTTP/1.1 200
Content-Type: application/json Content-Type: application/task+json
Content-Length: 142 Content-Length: 170
Date: Sun, 03 Oct 2021 17:27:06 GMT Date: Sun, 17 Oct 2021 21:07:04 GMT
{ {
"taskType" : "type1", "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskState" : "OPEN",
"taskListName" : "tapas-tasks-tutors",
"taskName":"task1", "taskName":"task1",
"taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" "taskType":"computation",
"taskStatus":"OPEN",
"originalTaskUri":"http://example.org",
"inputData":"1+1"
} }
``` ```
### Patching a task
REST emphasizes the generality of interfaces to promote uniform interaction. For instance, we can use
the `HTTP PATCH` method to implement fine-grained updates to the representational state of a task, which
may translate to various domain/integration events. However, to conform to the uniform interface
contraint in REST, any such updates have to rely on standard knowledge — and thus to hide away the
implementation details of our service.
In addition to the `application/task+json` media type we defined for our uniform HTTP API, a standard
representation format we can use to specify fine-grained updates to the representation of tasks
is [JSON Patch](http://jsonpatch.com/). In what follow, we provide a few examples of `HTTP PATCH` requests.
For further details on the JSON Patch format, see also [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)).
#### Changing the status of a task from OPEN to ASSIGNED
Sample HTTP request that assigns the previously created task to group `tapas-group1`:
```shell
curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \
--header 'Content-Type: application/json-patch+json' \
--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "ASSIGNED" },
{"op" : "add", "path": "/serviceProvider", "value" : "tapas-group1" } ]'
HTTP/1.1 200
Content-Type: application/task+json
Content-Length: 207
Date: Sun, 17 Oct 2021 21:20:58 GMT
{
"taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskName":"task1",
"taskType":"computation",
"taskStatus":"ASSIGNED",
"originalTaskUri":"http://example.org",
"serviceProvider":"tapas-group1",
"inputData":"1+1"
}
```
In this example, the requested patch includes two JSON Patch operations:
* an operation to `replace` the `taskStatus` already in the task's representation with the value `ASSIGNED`
* an operation to `add` to the task's representation a `serviceProvider` with the value `tapas-group1`
Internally, this request is mapped to a
[TaskAssignedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java).
The HTTP response returns a `200 OK` status code together with the updated representation of the task.
#### Changing the status of a task from to EXECUTED
Sample HTTP request that changes the status of the task to `EXECUTED` and adds an output result:
```shell
curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \
--header 'Content-Type: application/json-patch+json' \
--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "EXECUTED" },
{"op" : "add", "path": "/outputData", "value" : "2" } ]'
HTTP/1.1 200
Content-Type: application/task+json
Content-Length: 224
Date: Sun, 17 Oct 2021 21:32:25 GMT
{
"taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskName":"task1",
"taskType":"computation",
"taskStatus":"EXECUTED",
"originalTaskUri":"http://example.org",
"serviceProvider":"tapas-group1",
"inputData":"1+1",
"outputData":"2"
}
```
Internally, this request is mapped to a
[TaskExecutedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java).
The HTTP response returns a `200 OK` status code together with the updated representation of the task.

View File

@ -16,6 +16,12 @@
<properties> <properties>
<java.version>11</java.version> <java.version>11</java.version>
</properties> </properties>
<repositories>
<repository>
<id>Eclipse Paho Repo</id>
<url>https://repo.eclipse.org/content/repositories/paho-releases/</url>
</repository>
</repositories>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
@ -49,17 +55,16 @@
<version>1.1.0.Final</version> <version>1.1.0.Final</version>
</dependency> </dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
<dependency> <dependency>
<groupId>com.github.java-json-tools</groupId> <groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId> <artifactId>json-patch</artifactId>
<version>1.13</version> <version>1.13</version>
</dependency> </dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -3,13 +3,10 @@ package ch.unisg.tapastasks;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Collections;
@SpringBootApplication @SpringBootApplication
public class TapasTasksApplication { public class TapasTasksApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication tapasTasksApp = new SpringApplication(TapasTasksApplication.class); SpringApplication tapasTasksApp = new SpringApplication(TapasTasksApplication.class);
tapasTasksApp.run(args); tapasTasksApp.run(args);
} }

View File

@ -0,0 +1,102 @@
package ch.unisg.tapastasks.tasks.adapter.in.formats;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* This class is used to process JSON Patch operations for tasks: given a
* <a href="http://jsonpatch.com/">JSON Patch</a> for updating the representational state of a task,
* this class provides methods for extracting various operations of interest for our domain (e.g.,
* changing the status of a task).
*/
public class TaskJsonPatchRepresentation {
public static final String MEDIA_TYPE = "application/json-patch+json";
private final JsonNode patch;
/**
* Constructs the JSON Patch representation.
*
* @param patch a JSON Patch as JsonNode
*/
public TaskJsonPatchRepresentation(JsonNode patch) {
this.patch = patch;
}
/**
* Extracts the first task status replaced in this patch.
*
* @return the first task status changed in this patch or an empty {@link Optional} if none is
* found
*/
public Optional<Task.Status> extractFirstTaskStatusChange() {
Optional<JsonNode> status = extractFirst(node ->
isPatchReplaceOperation(node) && hasPath(node, "/taskStatus")
);
if (status.isPresent()) {
String taskStatus = status.get().get("value").asText();
return Optional.of(Task.Status.valueOf(taskStatus));
}
return Optional.empty();
}
/**
* Extracts the first service provider added or replaced in this patch.
*
* @return the first service provider changed in this patch or an empty {@link Optional} if none
* is found
*/
public Optional<Task.ServiceProvider> extractFirstServiceProviderChange() {
Optional<JsonNode> serviceProvider = extractFirst(node ->
(isPatchReplaceOperation(node) || isPatchAddOperation(node))
&& hasPath(node, "/serviceProvider")
);
return (serviceProvider.isEmpty()) ? Optional.empty()
: Optional.of(new Task.ServiceProvider(serviceProvider.get().get("value").asText()));
}
/**
* Extracts the first output data addition in this patch.
*
* @return the output data added in this patch or an empty {@link Optional} if none is found
*/
public Optional<Task.OutputData> extractFirstOutputDataAddition() {
Optional<JsonNode> output = extractFirst(node ->
isPatchAddOperation(node) && hasPath(node, "/outputData")
);
return (output.isEmpty()) ? Optional.empty()
: Optional.of(new Task.OutputData(output.get().get("value").asText()));
}
private Optional<JsonNode> extractFirst(Predicate<? super JsonNode> predicate) {
Stream<JsonNode> stream = StreamSupport.stream(patch.spliterator(), false);
return stream.filter(predicate).findFirst();
}
private boolean isPatchAddOperation(JsonNode node) {
return isPatchOperationOfType(node, "add");
}
private boolean isPatchReplaceOperation(JsonNode node) {
return isPatchOperationOfType(node, "replace");
}
private boolean isPatchOperationOfType(JsonNode node, String operation) {
return node.isObject() && node.get("op") != null
&& node.get("op").asText().equalsIgnoreCase(operation);
}
private boolean hasPath(JsonNode node, String path) {
return node.isObject() && node.get("path") != null
&& node.get("path").asText().equalsIgnoreCase(path);
}
}

View File

@ -0,0 +1,115 @@
package ch.unisg.tapastasks.tasks.adapter.in.formats;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
/**
* This class is used to expose and consume representations of tasks through the HTTP interface. The
* representations conform to the custom JSON-based media type "application/task+json". The media type
* is just an identifier and can be registered with
* <a href="https://www.iana.org/assignments/media-types/">IANA</a> to promote interoperability.
*/
final public class TaskJsonRepresentation {
// The media type used for this task representation format
public static final String MEDIA_TYPE = "application/task+json";
// A task identifier specific to our implementation (e.g., a UUID). This identifier is then used
// to generate the task's URI. URIs are standard uniform identifiers and use a universal syntax
// that can be referenced (and dereferenced) independent of context. In our uniform HTTP API,
// we identify tasks via URIs and not implementation-specific identifiers.
@Getter @Setter
private String taskId;
// A string that represents the task's name
@Getter
private final String taskName;
// A string that identifies the task's type. This string could also be a URI (e.g., defined in some
// Web ontology, as we shall see later in the course), but it's not constrained to be a URI.
// The task's type can be used to assign executors to tasks, to decide what tasks to bid for, etc.
@Getter
private final String taskType;
// The task's status: OPEN, ASSIGNED, RUNNING, or EXECUTED (see Task.Status)
@Getter @Setter
private String taskStatus;
// If this task is a delegated task (i.e., a shadow of another task), this URI points to the
// original task. Because URIs are standard and uniform, we can just dereference this URI to
// retrieve a representation of the original task.
@Getter @Setter
private String originalTaskUri;
// The service provider who executes this task. The service provider is a any string that identifies
// a TAPAS group (e.g., tapas-group1). This identifier could also be a URI (if we have a good reason
// for it), but it's not constrained to be a URI.
@Getter @Setter
private String serviceProvider;
// A string that provides domain-specific input data for this task. In the context of this project,
// we can parse and interpret the input data based on the task's type.
@Getter @Setter
private String inputData;
// A string that provides domain-specific output data for this task. In the context of this project,
// we can parse and interpret the output data based on the task's type.
@Getter @Setter
private String outputData;
/**
* Instantiate a task representation with a task name and type.
*
* @param taskName string that represents the task's name
* @param taskType string that represents the task's type
*/
public TaskJsonRepresentation(String taskName, String taskType) {
this.taskName = taskName;
this.taskType = taskType;
this.taskStatus = null;
this.originalTaskUri = null;
this.serviceProvider = null;
this.inputData = null;
this.outputData = null;
}
/**
* Instantiate a task representation from a domain concept.
*
* @param task the task
*/
public TaskJsonRepresentation(Task task) {
this(task.getTaskName().getValue(), task.getTaskType().getValue());
this.taskId = task.getTaskId().getValue();
this.taskStatus = task.getTaskStatus().getValue().name();
this.originalTaskUri = (task.getOriginalTaskUri() == null) ?
null : task.getOriginalTaskUri().getValue();
this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue();
this.inputData = (task.getInputData() == null) ? null : task.getInputData().getValue();
this.outputData = (task.getOutputData() == null) ? null : task.getOutputData().getValue();
}
/**
* Convenience method used to serialize a task provided as a domain concept in the format exposed
* through the uniform HTTP API.
*
* @param task the task as defined in the domain
* @return a string serialization using the JSON-based representation format defined for tasks
* @throws JsonProcessingException if a runtime exception occurs during object serialization
*/
public static String serialize(Task task) throws JsonProcessingException {
TaskJsonRepresentation representation = new TaskJsonRepresentation(task);
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper.writeValueAsString(representation);
}
}

View File

@ -0,0 +1,3 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging;
public class UnknownEventException extends RuntimeException { }

View File

@ -0,0 +1,39 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.application.handler.TaskAssignedHandler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.Task.TaskId;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
/**
* Listener for task assigned events. A task assigned event corresponds to a JSON Patch that attempts
* to change the task's status to ASSIGNED and may also add/replace a service provider (i.e., to what
* group the task was assigned). This implementation does not impose that a task assigned event
* includes the service provider (i.e., can be null).
*
* See also {@link TaskAssignedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
*/
public class TaskAssignedEventListenerHttpAdapter extends TaskEventListener {
/**
* Handles the task assigned event.
*
* @param taskId the identifier of the task for which an event was received
* @param payload the JSON Patch payload of the HTTP PATCH request received for this task
* @return
*/
public Task handleTaskEvent(String taskId, JsonNode payload) {
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
TaskAssignedEvent taskAssignedEvent = new TaskAssignedEvent(new TaskId(taskId), serviceProvider);
TaskAssignedEventHandler taskAssignedEventHandler = new TaskAssignedHandler();
return taskAssignedEventHandler.handleTaskAssigned(taskAssignedEvent);
}
}

View File

@ -0,0 +1,103 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.adapter.in.messaging.UnknownEventException;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jsonpatch.JsonPatch;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.util.Optional;
/**
* This REST Controller handles HTTP PATCH requests for updating the representational state of Task
* resources. Each request to update the representational state of a Task resource can correspond to
* at most one domain/integration event. Request payloads use the
* <a href="http://jsonpatch.com/">JSON PATCH</a> format and media type.
*
* A JSON Patch can contain multiple operations (e.g., add, remove, replace) for updating various
* parts of a task's representations. One or more JSON Patch operations can represent a domain/integration
* event. Therefore, the events can only be determined by inspecting the requested patch (e.g., a request
* to change a task's status from RUNNING to EXECUTED). This class is responsible to inspect requested
* patches, identify events, and to route them to appropriate listeners.
*
* For more details on JSON Patch, see: <a href="http://jsonpatch.com/">http://jsonpatch.com/</a>
* For some sample HTTP requests, see the README.
*/
@RestController
public class TaskEventHttpDispatcher {
// The standard media type for JSON Patch registered with IANA
// See: https://www.iana.org/assignments/media-types/application/json-patch+json
private final static String JSON_PATCH_MEDIA_TYPE = "application/json-patch+json";
/**
* Handles HTTP PATCH requests with a JSON Patch payload. Routes the requests based on the
* the operations requested in the patch. In this implementation, one HTTP Patch request is
* mapped to at most one domain event.
*
* @param taskId the local (i.e., implementation-specific) identifier of the task to the patched;
* this identifier is extracted from the task's URI
* @param payload the reuqested patch for this task
* @return 200 OK and a representation of the task after processing the event; 404 Not Found if
* the request URI does not match any task; 400 Bad Request if the request is invalid
*/
@PatchMapping(path = "/tasks/{taskId}", consumes = {JSON_PATCH_MEDIA_TYPE})
public ResponseEntity<String> dispatchTaskEvents(@PathVariable("taskId") String taskId,
@RequestBody JsonNode payload) {
try {
// Throw an exception if the JSON Patch format is invalid. This call is only used to
// validate the JSON PATCH syntax.
JsonPatch.fromJson(payload);
// Check for known events and route the events to appropriate listeners
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.Status> status = representation.extractFirstTaskStatusChange();
TaskEventListener listener = null;
// Route events related to task status changes
if (status.isPresent()) {
switch (status.get()) {
case ASSIGNED:
listener = new TaskAssignedEventListenerHttpAdapter();
break;
case RUNNING:
listener = new TaskStartedEventListenerHttpAdapter();
break;
case EXECUTED:
listener = new TaskExecutedEventListenerHttpAdapter();
break;
}
}
if (listener == null) {
// The HTTP PATCH request is valid, but the patch does not match any known event
throw new UnknownEventException();
}
Task task = listener.handleTaskEvent(taskId, payload);
// Add the content type as a response header
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
return new ResponseEntity<>(TaskJsonRepresentation.serialize(task), responseHeaders,
HttpStatus.OK);
} catch (TaskNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
} catch (IOException | RuntimeException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
}

View File

@ -0,0 +1,24 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Abstract class that handles events specific to a Task. Events are received via an HTTP PATCH
* request for a given task and dispatched to Task event listeners (see {@link TaskEventHttpDispatcher}).
* Each listener must implement the abstract method {@link #handleTaskEvent(String, JsonNode)}, which
* may require additional event-specific validations.
*/
public abstract class TaskEventListener {
/**
* This abstract method handles a task event and returns the task after the event was handled.
*
* @param taskId the identifier of the task for which an event was received
* @param payload the JSON Patch payload of the HTTP PATCH request received for this task
* @return the task for which the HTTP PATCH request is handled
* @throws TaskNotFoundException
*/
public abstract Task handleTaskEvent(String taskId, JsonNode payload) throws TaskNotFoundException;
}

View File

@ -0,0 +1,34 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.application.handler.TaskExecutedHandler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
/**
* Listener for task executed events. A task executed event corresponds to a JSON Patch that attempts
* to change the task's status to EXECUTED, may add/replace a service provider, and may also add an
* output result. This implementation does not impose that a task executed event includes either the
* service provider or an output result (i.e., both can be null).
*
* See also {@link TaskExecutedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
*/
public class TaskExecutedEventListenerHttpAdapter extends TaskEventListener {
public Task handleTaskEvent(String taskId, JsonNode payload) {
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
Optional<Task.OutputData> outputData = representation.extractFirstOutputDataAddition();
TaskExecutedEvent taskExecutedEvent = new TaskExecutedEvent(new Task.TaskId(taskId),
serviceProvider, outputData);
TaskExecutedEventHandler taskExecutedEventHandler = new TaskExecutedHandler();
return taskExecutedEventHandler.handleTaskExecuted(taskExecutedEvent);
}
}

View File

@ -0,0 +1,32 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.application.handler.TaskStartedHandler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.Task.TaskId;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
/**
* Listener for task started events. A task started event corresponds to a JSON Patch that attempts
* to change the task's status to RUNNING and may also add/replace a service provider. This
* implementation does not impose that a task started event includes the service provider (i.e.,
* can be null).
*
* See also {@link TaskStartedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
*/
public class TaskStartedEventListenerHttpAdapter extends TaskEventListener {
public Task handleTaskEvent(String taskId, JsonNode payload) {
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
TaskStartedEvent taskStartedEvent = new TaskStartedEvent(new TaskId(taskId), serviceProvider);
TaskStartedEventHandler taskStartedEventHandler = new TaskStartedHandler();
return taskStartedEventHandler.handleTaskStarted(taskStartedEvent);
}
}

View File

@ -1,8 +1,12 @@
package ch.unisg.tapastasks.tasks.adapter.in.web; package ch.unisg.tapastasks.tasks.adapter.in.web;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand; import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase; import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase;
import ch.unisg.tapastasks.tasks.domain.Task; import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -12,29 +16,67 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import javax.validation.ConstraintViolationException; import javax.validation.ConstraintViolationException;
import java.util.Optional;
/**
* Controller that handles HTTP requests for creating new tasks. This controller implements the
* {@link AddNewTaskToTaskListUseCase} use case using the {@link AddNewTaskToTaskListCommand}.
*
* A new task is created via an HTTP POST request to the /tasks/ endpoint. The body of the request
* contains a JSON-based representation with the "application/task+json" media type defined for this
* project. This custom media type allows to capture the semantics of our JSON representations for
* tasks.
*
* If the request is successful, the controller returns an HTTP 201 Created status code and a
* representation of the created task with Content-Type "application/task+json". The HTTP response
* also include a Location header field that points to the URI of the created task.
*/
@RestController @RestController
public class AddNewTaskToTaskListWebController { public class AddNewTaskToTaskListWebController {
private final AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase; private final AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase;
// Used to retrieve properties from application.properties
@Autowired
private Environment environment;
public AddNewTaskToTaskListWebController(AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase) { public AddNewTaskToTaskListWebController(AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase) {
this.addNewTaskToTaskListUseCase = addNewTaskToTaskListUseCase; this.addNewTaskToTaskListUseCase = addNewTaskToTaskListUseCase;
} }
@PostMapping(path = "/tasks/", consumes = {TaskMediaType.TASK_MEDIA_TYPE}) @PostMapping(path = "/tasks/", consumes = {TaskJsonRepresentation.MEDIA_TYPE})
public ResponseEntity<String> addNewTaskTaskToTaskList(@RequestBody Task task) { public ResponseEntity<String> addNewTaskTaskToTaskList(@RequestBody TaskJsonRepresentation payload) {
try { try {
AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand( Task.TaskName taskName = new Task.TaskName(payload.getTaskName());
task.getTaskName(), task.getTaskType() Task.TaskType taskType = new Task.TaskType(payload.getTaskType());
);
Task newTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command); // If the created task is a delegated task, the representation contains a URI reference
// to the original task
Optional<Task.OriginalTaskUri> originalTaskUriOptional =
(payload.getOriginalTaskUri() == null) ? Optional.empty()
: Optional.of(new Task.OriginalTaskUri(payload.getOriginalTaskUri()));
AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(taskName, taskType,
originalTaskUriOptional);
Task createdTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command);
// When creating a task, the task's representation may include optional input data
if (payload.getInputData() != null) {
createdTask.setInputData(new Task.InputData(payload.getInputData()));
}
// Add the content type as a response header // Add the content type as a response header
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
// Construct and advertise the URI of the newly created task; we retrieve the base URI
// from the application.properties file
responseHeaders.add(HttpHeaders.LOCATION, environment.getProperty("baseuri")
+ "tasks/" + createdTask.getTaskId().getValue());
return new ResponseEntity<>(TaskMediaType.serialize(newTask), responseHeaders, HttpStatus.CREATED); return new ResponseEntity<>(TaskJsonRepresentation.serialize(createdTask), responseHeaders,
HttpStatus.CREATED);
} catch (JsonProcessingException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
} catch (ConstraintViolationException e) { } catch (ConstraintViolationException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
} }

View File

@ -1,8 +1,10 @@
package ch.unisg.tapastasks.tasks.adapter.in.web; package ch.unisg.tapastasks.tasks.adapter.in.web;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListQuery; import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListQuery;
import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase; import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase;
import ch.unisg.tapastasks.tasks.domain.Task; import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@ -11,6 +13,11 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.Optional; import java.util.Optional;
/**
* Controller that handles HTTP GET requests for retrieving tasks. This controller implements the
* {@link RetrieveTaskFromTaskListUseCase} use case using the {@link RetrieveTaskFromTaskListQuery}
* query.
*/
@RestController @RestController
public class RetrieveTaskFromTaskListWebController { public class RetrieveTaskFromTaskListWebController {
private final RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase; private final RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase;
@ -19,10 +26,17 @@ public class RetrieveTaskFromTaskListWebController {
this.retrieveTaskFromTaskListUseCase = retrieveTaskFromTaskListUseCase; this.retrieveTaskFromTaskListUseCase = retrieveTaskFromTaskListUseCase;
} }
/**
* Retrieves a representation of task. Returns HTTP 200 OK if the request is successful with a
* representation of the task using the Content-Type "applicatoin/task+json".
*
* @param taskId the local identifier of the requested task (extracted from the task's URI)
* @return a representation of the task if the task exists
*/
@GetMapping(path = "/tasks/{taskId}") @GetMapping(path = "/tasks/{taskId}")
public ResponseEntity<String> retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) { public ResponseEntity<String> retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) {
RetrieveTaskFromTaskListQuery command = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId)); RetrieveTaskFromTaskListQuery query = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId));
Optional<Task> updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(command); Optional<Task> updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(query);
// Check if the task with the given identifier exists // Check if the task with the given identifier exists
if (updatedTaskOpt.isEmpty()) { if (updatedTaskOpt.isEmpty()) {
@ -30,11 +44,16 @@ public class RetrieveTaskFromTaskListWebController {
throw new ResponseStatusException(HttpStatus.NOT_FOUND); throw new ResponseStatusException(HttpStatus.NOT_FOUND);
} }
try {
String taskRepresentation = TaskJsonRepresentation.serialize(updatedTaskOpt.get());
// Add the content type as a response header // Add the content type as a response header
HttpHeaders responseHeaders = new HttpHeaders(); HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
return new ResponseEntity<>(TaskMediaType.serialize(updatedTaskOpt.get()), responseHeaders, return new ResponseEntity<>(taskRepresentation, responseHeaders, HttpStatus.OK);
HttpStatus.OK); } catch (JsonProcessingException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
} }
} }

View File

@ -1,23 +0,0 @@
package ch.unisg.tapastasks.tasks.adapter.in.web;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import org.json.JSONObject;
final public class TaskMediaType {
public static final String TASK_MEDIA_TYPE = "application/json";
public static String serialize(Task task) {
JSONObject payload = new JSONObject();
payload.put("taskId", task.getTaskId().getValue());
payload.put("taskName", task.getTaskName().getValue());
payload.put("taskType", task.getTaskType().getValue());
payload.put("taskState", task.getTaskState().getValue());
payload.put("taskListName", TaskList.getTapasTaskList().getTaskListName().getValue());
return payload.toString();
}
private TaskMediaType() { }
}

View File

@ -0,0 +1,19 @@
package ch.unisg.tapastasks.tasks.application.handler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException;
import org.springframework.stereotype.Component;
@Component
public class TaskAssignedHandler implements TaskAssignedEventHandler {
@Override
public Task handleTaskAssigned(TaskAssignedEvent taskAssignedEvent) throws TaskNotFoundException {
TaskList taskList = TaskList.getTapasTaskList();
return taskList.changeTaskStatusToAssigned(taskAssignedEvent.getTaskId(),
taskAssignedEvent.getServiceProvider());
}
}

View File

@ -0,0 +1,19 @@
package ch.unisg.tapastasks.tasks.application.handler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException;
import org.springframework.stereotype.Component;
@Component
public class TaskExecutedHandler implements TaskExecutedEventHandler {
@Override
public Task handleTaskExecuted(TaskExecutedEvent taskExecutedEvent) throws TaskNotFoundException {
TaskList taskList = TaskList.getTapasTaskList();
return taskList.changeTaskStatusToExecuted(taskExecutedEvent.getTaskId(),
taskExecutedEvent.getServiceProvider(), taskExecutedEvent.getOutputData());
}
}

View File

@ -0,0 +1,19 @@
package ch.unisg.tapastasks.tasks.application.handler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskList;
import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException;
import org.springframework.stereotype.Component;
@Component
public class TaskStartedHandler implements TaskStartedEventHandler {
@Override
public Task handleTaskStarted(TaskStartedEvent taskStartedEvent) throws TaskNotFoundException {
TaskList taskList = TaskList.getTapasTaskList();
return taskList.changeTaskStatusToRunning(taskStartedEvent.getTaskId(),
taskStartedEvent.getServiceProvider());
}
}

View File

@ -1,23 +1,30 @@
package ch.unisg.tapastasks.tasks.application.port.in; package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating; import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task.TaskType; import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.Task.TaskName; import lombok.Getter;
import lombok.Value; import lombok.Value;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value @Value
public class AddNewTaskToTaskListCommand extends SelfValidating<AddNewTaskToTaskListCommand> { public class AddNewTaskToTaskListCommand extends SelfValidating<AddNewTaskToTaskListCommand> {
@NotNull @NotNull
private final TaskName taskName; private final Task.TaskName taskName;
@NotNull @NotNull
private final TaskType taskType; private final Task.TaskType taskType;
public AddNewTaskToTaskListCommand(TaskName taskName, TaskType taskType) { @Getter
private final Optional<Task.OriginalTaskUri> originalTaskUri;
public AddNewTaskToTaskListCommand(Task.TaskName taskName, Task.TaskType taskType,
Optional<Task.OriginalTaskUri> originalTaskUri) {
this.taskName = taskName; this.taskName = taskName;
this.taskType = taskType; this.taskType = taskType;
this.originalTaskUri = originalTaskUri;
this.validateSelf(); this.validateSelf();
} }
} }

View File

@ -5,5 +5,5 @@ import ch.unisg.tapastasks.tasks.domain.Task;
import java.util.Optional; import java.util.Optional;
public interface RetrieveTaskFromTaskListUseCase { public interface RetrieveTaskFromTaskListUseCase {
Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command); Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query);
} }

View File

@ -0,0 +1,25 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task;
import lombok.Getter;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value
public class TaskAssignedEvent extends SelfValidating<TaskAssignedEvent> {
@NotNull
private final Task.TaskId taskId;
@Getter
private final Optional<Task.ServiceProvider> serviceProvider;
public TaskAssignedEvent(Task.TaskId taskId, Optional<Task.ServiceProvider> serviceProvider) {
this.taskId = taskId;
this.serviceProvider = serviceProvider;
this.validateSelf();
}
}

View File

@ -0,0 +1,8 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.tasks.domain.Task;
public interface TaskAssignedEventHandler {
Task handleTaskAssigned(TaskAssignedEvent taskStartedEvent);
}

View File

@ -0,0 +1,34 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task.*;
import lombok.Getter;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value
public class TaskExecutedEvent extends SelfValidating<TaskExecutedEvent> {
@NotNull
private final TaskId taskId;
@Getter
private final Optional<ServiceProvider> serviceProvider;
@Getter
private final Optional<OutputData> outputData;
public TaskExecutedEvent(TaskId taskId, Optional<ServiceProvider> serviceProvider,
Optional<OutputData> outputData) {
this.taskId = taskId;
this.serviceProvider = serviceProvider;
this.outputData = outputData;
this.validateSelf();
}
}

View File

@ -0,0 +1,8 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.tasks.domain.Task;
public interface TaskExecutedEventHandler {
Task handleTaskExecuted(TaskExecutedEvent taskExecutedEvent);
}

View File

@ -0,0 +1,28 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task;
import lombok.Getter;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value
public class TaskStartedEvent extends SelfValidating<TaskStartedEvent> {
@NotNull
private final Task.TaskId taskId;
@Getter
private final Optional<Task.ServiceProvider> serviceProvider;
public TaskStartedEvent(Task.TaskId taskId, Optional<Task.ServiceProvider> serviceProvider) {
this.taskId = taskId;
this.serviceProvider = serviceProvider;
this.validateSelf();
}
}

View File

@ -0,0 +1,8 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.tasks.domain.Task;
public interface TaskStartedEventHandler {
Task handleTaskStarted(TaskStartedEvent taskStartedEvent);
}

View File

@ -21,7 +21,13 @@ public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase
@Override @Override
public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) { public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) {
TaskList taskList = TaskList.getTapasTaskList(); TaskList taskList = TaskList.getTapasTaskList();
Task newTask = taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType());
Task newTask = (command.getOriginalTaskUri().isPresent()) ?
// Create a delegated task that points back to the original task
taskList.addNewTaskWithNameAndTypeAndOriginalTaskUri(command.getTaskName(),
command.getTaskType(), command.getOriginalTaskUri().get())
// Create an original task
: taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType());
//Here we are using the application service to emit the domain event to the outside of the bounded context. //Here we are using the application service to emit the domain event to the outside of the bounded context.
//This event should be considered as a light-weight "integration event" to communicate with other services. //This event should be considered as a light-weight "integration event" to communicate with other services.

View File

@ -15,8 +15,8 @@ import java.util.Optional;
@Transactional @Transactional
public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase { public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase {
@Override @Override
public Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command) { public Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query) {
TaskList taskList = TaskList.getTapasTaskList(); TaskList taskList = TaskList.getTapasTaskList();
return taskList.retrieveTaskById(command.getTaskId()); return taskList.retrieveTaskById(query.getTaskId());
} }
} }

View File

@ -8,7 +8,7 @@ import java.util.UUID;
/**This is a domain entity**/ /**This is a domain entity**/
public class Task { public class Task {
public enum State { public enum Status {
OPEN, ASSIGNED, RUNNING, EXECUTED OPEN, ASSIGNED, RUNNING, EXECUTED
} }
@ -22,38 +22,81 @@ public class Task {
private final TaskType taskType; private final TaskType taskType;
@Getter @Getter
private TaskState taskState; private final OriginalTaskUri originalTaskUri;
@Getter @Setter
private TaskStatus taskStatus;
@Getter @Setter
private ServiceProvider provider;
@Getter @Setter
private InputData inputData;
@Getter @Setter
private OutputData outputData;
public Task(TaskName taskName, TaskType taskType, OriginalTaskUri taskUri) {
this.taskId = new TaskId(UUID.randomUUID().toString());
public Task(TaskName taskName, TaskType taskType) {
this.taskName = taskName; this.taskName = taskName;
this.taskType = taskType; this.taskType = taskType;
this.taskState = new TaskState(State.OPEN); this.originalTaskUri = taskUri;
this.taskId = new TaskId(UUID.randomUUID().toString());
this.taskStatus = new TaskStatus(Status.OPEN);
this.inputData = null;
this.outputData = null;
} }
protected static Task createTaskWithNameAndType(TaskName name, TaskType type) { protected static Task createTaskWithNameAndType(TaskName name, TaskType type) {
//This is a simple debug message to see that the request has reached the right method in the core //This is a simple debug message to see that the request has reached the right method in the core
System.out.println("New Task: " + name.getValue() + " " + type.getValue()); System.out.println("New Task: " + name.getValue() + " " + type.getValue());
return new Task(name,type); return new Task(name, type, null);
}
protected static Task createTaskWithNameAndTypeAndOriginalTaskUri(TaskName name, TaskType type,
OriginalTaskUri originalTaskUri) {
return new Task(name, type, originalTaskUri);
} }
@Value @Value
public static class TaskId { public static class TaskId {
private String value; String value;
} }
@Value @Value
public static class TaskName { public static class TaskName {
private String value; String value;
}
@Value
public static class TaskState {
private State value;
} }
@Value @Value
public static class TaskType { public static class TaskType {
private String value; String value;
}
@Value
public static class OriginalTaskUri {
String value;
}
@Value
public static class TaskStatus {
Status value;
}
@Value
public static class ServiceProvider {
String value;
}
@Value
public static class InputData {
String value;
}
@Value
public static class OutputData {
String value;
} }
} }

View File

@ -3,7 +3,6 @@ package ch.unisg.tapastasks.tasks.domain;
import lombok.Getter; import lombok.Getter;
import lombok.Value; import lombok.Value;
import javax.swing.text.html.Option;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -20,7 +19,7 @@ public class TaskList {
//Note: We do not care about the management of task lists, there is only one within this service //Note: We do not care about the management of task lists, there is only one within this service
//--> using the Singleton pattern here to make lives easy; we will later load it from a repo //--> using the Singleton pattern here to make lives easy; we will later load it from a repo
//TODO change "tutors" to your group name ("groupx") //TODO: change "tutors" to your group name ("groupx")
private static final TaskList taskList = new TaskList(new TaskListName("tapas-tasks-tutors")); private static final TaskList taskList = new TaskList(new TaskListName("tapas-tasks-tutors"));
private TaskList(TaskListName taskListName) { private TaskList(TaskListName taskListName) {
@ -36,13 +35,26 @@ public class TaskList {
//Note: Here we could add some sophisticated invariants/business rules that the aggregate root checks //Note: Here we could add some sophisticated invariants/business rules that the aggregate root checks
public Task addNewTaskWithNameAndType(Task.TaskName name, Task.TaskType type) { public Task addNewTaskWithNameAndType(Task.TaskName name, Task.TaskType type) {
Task newTask = Task.createTaskWithNameAndType(name, type); Task newTask = Task.createTaskWithNameAndType(name, type);
listOfTasks.value.add(newTask); this.addNewTaskToList(newTask);
//This is a simple debug message to see that the task list is growing with each new request
System.out.println("Number of tasks: "+listOfTasks.value.size()); return newTask;
}
public Task addNewTaskWithNameAndTypeAndOriginalTaskUri(Task.TaskName name, Task.TaskType type,
Task.OriginalTaskUri originalTaskUri) {
Task newTask = Task.createTaskWithNameAndTypeAndOriginalTaskUri(name, type, originalTaskUri);
this.addNewTaskToList(newTask);
return newTask;
}
private void addNewTaskToList(Task newTask) {
//Here we would also publish a domain event to other entities in the core interested in this event. //Here we would also publish a domain event to other entities in the core interested in this event.
//However, we skip this here as it makes the core even more complex (e.g., we have to implement a light-weight //However, we skip this here as it makes the core even more complex (e.g., we have to implement a light-weight
//domain event publisher and subscribers (see "Implementing Domain-Driven Design by V. Vernon, pp. 296ff). //domain event publisher and subscribers (see "Implementing Domain-Driven Design by V. Vernon, pp. 296ff).
return newTask; listOfTasks.value.add(newTask);
//This is a simple debug message to see that the task list is growing with each new request
System.out.println("Number of tasks: " + listOfTasks.value.size());
} }
public Optional<Task> retrieveTaskById(Task.TaskId id) { public Optional<Task> retrieveTaskById(Task.TaskId id) {
@ -55,6 +67,43 @@ public class TaskList {
return Optional.empty(); return Optional.empty();
} }
public Task changeTaskStatusToAssigned(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider)
throws TaskNotFoundException {
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty());
}
public Task changeTaskStatusToRunning(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider)
throws TaskNotFoundException {
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty());
}
public Task changeTaskStatusToExecuted(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider,
Optional<Task.OutputData> outputData) throws TaskNotFoundException {
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData);
}
private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional<Task.ServiceProvider> serviceProvider,
Optional<Task.OutputData> outputData) {
Optional<Task> taskOpt = retrieveTaskById(id);
if (taskOpt.isEmpty()) {
throw new TaskNotFoundException();
}
Task task = taskOpt.get();
task.setTaskStatus(status);
if (serviceProvider.isPresent()) {
task.setProvider(serviceProvider.get());
}
if (outputData.isPresent()) {
task.setOutputData(outputData.get());
}
return task;
}
@Value @Value
public static class TaskListName { public static class TaskListName {
private String value; private String value;
@ -64,5 +113,4 @@ public class TaskList {
public static class ListOfTasks { public static class ListOfTasks {
private List<Task> value; private List<Task> value;
} }
} }

View File

@ -0,0 +1,3 @@
package ch.unisg.tapastasks.tasks.domain;
public class TaskNotFoundException extends RuntimeException { }

View File

@ -1 +1,2 @@
server.port=8081 server.port=8081
baseuri=https://tapas-tasks.86-119-34-23.nip.io/