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

@@ -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() {
}
}