diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml
index 619804e..2c5b192 100644
--- a/.github/workflows/build-and-deploy.yml
+++ b/.github/workflows/build-and-deploy.yml
@@ -27,7 +27,11 @@ jobs:
- name: Build with Maven
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
run: mvn -f app/pom.xml --batch-mode --update-snapshots verify
diff --git a/docker-compose.yml b/docker-compose.yml
index 6d8f256..dad6b78 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -41,6 +41,21 @@ services:
- "traefik.http.routers.tapas-tasks.entryPoints=web,websecure"
- "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:
image: openjdk
command: "java -jar /data/app-0.1.0.jar"
diff --git a/tapas-auction-house/.editorconfig b/tapas-auction-house/.editorconfig
new file mode 100644
index 0000000..c4f3e5b
--- /dev/null
+++ b/tapas-auction-house/.editorconfig
@@ -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
diff --git a/tapas-auction-house/.gitignore b/tapas-auction-house/.gitignore
new file mode 100644
index 0000000..549e00a
--- /dev/null
+++ b/tapas-auction-house/.gitignore
@@ -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/
diff --git a/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java b/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java
new file mode 100644
index 0000000..e76d1f3
--- /dev/null
+++ b/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java
@@ -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();
+ }
+
+}
diff --git a/tapas-auction-house/.mvn/wrapper/maven-wrapper.jar b/tapas-auction-house/.mvn/wrapper/maven-wrapper.jar
new file mode 100644
index 0000000..2cc7d4a
Binary files /dev/null and b/tapas-auction-house/.mvn/wrapper/maven-wrapper.jar differ
diff --git a/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties b/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000..ffdc10e
--- /dev/null
+++ b/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties
@@ -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
diff --git a/tapas-auction-house/README.md b/tapas-auction-house/README.md
new file mode 100644
index 0000000..683500d
--- /dev/null
+++ b/tapas-auction-house/README.md
@@ -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" }'
+```
diff --git a/tapas-auction-house/mvnw b/tapas-auction-house/mvnw
new file mode 100755
index 0000000..a16b543
--- /dev/null
+++ b/tapas-auction-house/mvnw
@@ -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 "$@"
diff --git a/tapas-auction-house/mvnw.cmd b/tapas-auction-house/mvnw.cmd
new file mode 100644
index 0000000..c8d4337
--- /dev/null
+++ b/tapas-auction-house/mvnw.cmd
@@ -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%
diff --git a/tapas-auction-house/pom.xml b/tapas-auction-house/pom.xml
new file mode 100644
index 0000000..4b9cbb6
--- /dev/null
+++ b/tapas-auction-house/pom.xml
@@ -0,0 +1,80 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.5.3
+
+
+ ch.unisg
+ tapas-auction-house
+ 0.0.1-SNAPSHOT
+ tapas-auction-house
+ TAPAS Auction House
+
+ 11
+
+
+
+ Eclipse Paho Repo
+ https://repo.eclipse.org/content/repositories/paho-releases/
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.eclipse.paho
+ org.eclipse.paho.client.mqttv3
+ 1.2.0
+
+
+ javax.transaction
+ javax.transaction-api
+ 1.2
+
+
+ javax.validation
+ validation-api
+ 1.1.0.Final
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+
+
+
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java
new file mode 100644
index 0000000..8fc22d0
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java
@@ -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 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 discoverAuctionHouseEndpoints() {
+ AuctionHouseResourceDirectory rd = new AuctionHouseResourceDirectory(
+ URI.create(RESOURCE_DIRECTORY)
+ );
+
+ return rd.retrieveAuctionHouseEndpoints();
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java
new file mode 100644
index 0000000..708d512
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java
@@ -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) { }
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java
new file mode 100644
index 0000000..da2b096
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java
@@ -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
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java
new file mode 100644
index 0000000..4500423
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java
@@ -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);
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java
new file mode 100644
index 0000000..999c61c
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java
@@ -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 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);
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java
new file mode 100644
index 0000000..53811f9
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java
@@ -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
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java
new file mode 100644
index 0000000..6da39e6
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java
@@ -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);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java
new file mode 100644
index 0000000..e5eaf12
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java
@@ -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 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 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);
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java
new file mode 100644
index 0000000..87413f0
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java
@@ -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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java
new file mode 100644
index 0000000..d156452
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java
@@ -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
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java
new file mode 100644
index 0000000..c65631e
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java
@@ -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 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);
+ }
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java
new file mode 100644
index 0000000..bcbf38c
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java
@@ -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 retrieveOpenAuctions() {
+ Collection 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, "; rel=\"hub\"");
+ responseHeaders.add(HttpHeaders.LINK, "; rel=\"self\"");
+
+ return new ResponseEntity<>(array.toString(), responseHeaders, HttpStatus.OK);
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java
new file mode 100644
index 0000000..9e6ec67
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java
@@ -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
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java
new file mode 100644
index 0000000..26949f2
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java
@@ -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) {
+
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java
new file mode 100644
index 0000000..6db8c68
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java
@@ -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
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java
new file mode 100644
index 0000000..e4b312f
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java
@@ -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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java
new file mode 100644
index 0000000..624e669
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java
@@ -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());
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java
new file mode 100644
index 0000000..c3bfed8
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java
@@ -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());
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java
new file mode 100644
index 0000000..b937e26
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java
@@ -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 {
+ @NotNull
+ private final Auction auction;
+
+ public AuctionStartedEvent(Auction auction) {
+ this.auction = auction;
+ this.validateSelf();
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java
new file mode 100644
index 0000000..1eed1d9
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java
@@ -0,0 +1,6 @@
+package ch.unisg.tapas.auctionhouse.application.port.in;
+
+public interface AuctionStartedEventHandler {
+
+ boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java
new file mode 100644
index 0000000..5a53b94
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java
@@ -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 {
+ @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();
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java
new file mode 100644
index 0000000..ca82a1c
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java
@@ -0,0 +1,6 @@
+package ch.unisg.tapas.auctionhouse.application.port.in;
+
+public interface ExecutorAddedEventHandler {
+
+ boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java
new file mode 100644
index 0000000..4d5c910
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java
@@ -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 {
+ @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();
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java
new file mode 100644
index 0000000..6d92422
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java
@@ -0,0 +1,6 @@
+package ch.unisg.tapas.auctionhouse.application.port.in;
+
+public interface ExecutorRemovedEventHandler {
+
+ boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java
new file mode 100644
index 0000000..626fa49
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java
@@ -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 {
+ @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();
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java
new file mode 100644
index 0000000..261240b
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java
@@ -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);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java
new file mode 100644
index 0000000..a77f267
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java
@@ -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 { }
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java
new file mode 100644
index 0000000..4f94df2
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java
@@ -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 retrieveAuctions(RetrieveOpenAuctionsQuery query);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java
new file mode 100644
index 0000000..9a432c9
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java
@@ -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);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java
new file mode 100644
index 0000000..7ed440f
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java
@@ -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);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java
new file mode 100644
index 0000000..e207891
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java
@@ -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 {
+ @NotNull
+ private final Auction auction;
+ @NotNull
+ private final Bid bid;
+
+ public PlaceBidForAuctionCommand(Auction auction, Bid bid) {
+ this.auction = auction;
+ this.bid = bid;
+ this.validateSelf();
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java
new file mode 100644
index 0000000..3bf5a16
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java
@@ -0,0 +1,6 @@
+package ch.unisg.tapas.auctionhouse.application.port.out;
+
+public interface PlaceBidForAuctionCommandPort {
+
+ void placeBid(PlaceBidForAuctionCommand command);
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java
new file mode 100644
index 0000000..bdba393
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java
@@ -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 retrieveAuctions(RetrieveOpenAuctionsQuery query) {
+ return AuctionRegistry.getInstance().getOpenAuctions();
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java
new file mode 100644
index 0000000..42c6e37
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java
@@ -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 auctionOpt = auctions.getAuctionById(auctionId);
+
+ if (auctionOpt.isPresent()) {
+ Auction auction = auctionOpt.get();
+ Optional 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());
+ }
+ }
+ }
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java
new file mode 100644
index 0000000..3e51ef7
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java
@@ -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 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 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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java
new file mode 100644
index 0000000..858589d
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java
@@ -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 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 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 getAllAuctions() {
+ return auctions.values();
+ }
+
+ /**
+ * Retrieves only the auctions that are open for bids.
+ *
+ * @return a collection with all open auctions
+ */
+ public Collection getOpenAuctions() {
+ return getAllAuctions()
+ .stream()
+ .filter(auction -> auction.isOpen())
+ .collect(Collectors.toList());
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java
new file mode 100644
index 0000000..7cac1ec
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java
@@ -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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java
new file mode 100644
index 0000000..484646c
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java
@@ -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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java
new file mode 100644
index 0000000..4f24f30
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java
@@ -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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java
new file mode 100644
index 0000000..9da3756
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java
@@ -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> 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 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 iterator = executors.keySet().iterator();
+
+ while (iterator.hasNext()) {
+ Auction.AuctionedTaskType taskType = iterator.next();
+ Set 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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java
new file mode 100644
index 0000000..c4809ef
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java
@@ -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 retrieveAuctionHouseEndpoints() {
+ List auctionHouseEndpoints = new ArrayList<>();
+
+ try {
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(rdEndpoint).GET().build();
+
+ HttpResponse 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;
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java
new file mode 100644
index 0000000..748afda
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java
@@ -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"));
+ }
+}
diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java
new file mode 100644
index 0000000..1b56db4
--- /dev/null
+++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java
@@ -0,0 +1,25 @@
+package ch.unisg.tapas.common;
+
+import javax.validation.*;
+import java.util.Set;
+
+public class SelfValidating {
+
+ 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> violations = validator.validate((T) this);
+ if (!violations.isEmpty()) {
+ throw new ConstraintViolationException(violations);
+ }
+ }
+}
diff --git a/tapas-auction-house/src/main/resources/application.properties b/tapas-auction-house/src/main/resources/application.properties
new file mode 100644
index 0000000..2c92c87
--- /dev/null
+++ b/tapas-auction-house/src/main/resources/application.properties
@@ -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/
diff --git a/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java b/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java
new file mode 100644
index 0000000..ce414c3
--- /dev/null
+++ b/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java
@@ -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() {
+ }
+
+}
diff --git a/tapas-tasks/README.md b/tapas-tasks/README.md
index f083776..90016c3 100644
--- a/tapas-tasks/README.md
+++ b/tapas-tasks/README.md
@@ -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.
## HTTP API Overview
-The code we provide includes a minimalistic HTTP API for (i) creating a new task and (ii) retrieving
-the representation of a task.
+The code we provide includes a minimalistic uniform HTTP API for (i) creating a new task, (ii) retrieving
+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/).
### Creating a new task
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
* `taskType`: a string that represents the type of the task to be created
A sample HTTP request with `curl`:
```shell
-curl -i --location --request POST 'http://localhost:8081/tasks/' --header 'Content-Type: application/json' --data-raw '{
- "taskName" : "task1",
- "taskType" : "type1"
+curl -i --location --request POST 'http://localhost:8081/tasks/' \
+--header 'Content-Type: application/task+json' \
+--data-raw '{
+ "taskName" : "task1",
+ "taskType" : "computation",
+ "originalTaskUri" : "http://example.org",
+ "inputData" : "1+1"
}'
HTTP/1.1 201
-Content-Type: application/json
-Content-Length: 142
-Date: Sun, 03 Oct 2021 17:25:32 GMT
+Location: http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354
+Content-Type: application/task+json
+Content-Length: 170
+Date: Sun, 17 Oct 2021 21:03:34 GMT
{
- "taskType" : "type1",
- "taskState" : "OPEN",
- "taskListName" : "tapas-tasks-tutors",
- "taskName" : "task1",
- "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0"
+ "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
+ "taskName":"task1",
+ "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
-representation of the created task. The representation includes, among others, a _universally unique
-identifier (UUID)_ for the newly created task (`taskId`).
+If the task is created successfuly, a `201 Created` status code is returned together with a
+representation of the created task. The response also includes a `Location` header filed that points
+to the URI of the newly created task.
### Retrieving a task
-The representation of a task is retrieved via an `HTTP GET` request to the `/tasks/` endpoint.
+The representation of a task is retrieved via an `HTTP GET` request to the URI of task.
A sample HTTP request with `curl`:
```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
-Content-Type: application/json
-Content-Length: 142
-Date: Sun, 03 Oct 2021 17:27:06 GMT
+Content-Type: application/task+json
+Content-Length: 170
+Date: Sun, 17 Oct 2021 21:07:04 GMT
{
- "taskType" : "type1",
- "taskState" : "OPEN",
- "taskListName" : "tapas-tasks-tutors",
- "taskName" : "task1",
- "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0"
+ "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
+ "taskName":"task1",
+ "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.
diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml
index a5b6587..3eac732 100644
--- a/tapas-tasks/pom.xml
+++ b/tapas-tasks/pom.xml
@@ -16,6 +16,12 @@
11
+
+
+ Eclipse Paho Repo
+ https://repo.eclipse.org/content/repositories/paho-releases/
+
+
org.springframework.boot
@@ -49,17 +55,16 @@
1.1.0.Final
-
- org.json
- json
- 20210307
-
com.github.java-json-tools
json-patch
1.13
-
+
+ org.eclipse.paho
+ org.eclipse.paho.client.mqttv3
+ 1.2.0
+
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java
index 40fa5da..2675391 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java
@@ -3,13 +3,10 @@ package ch.unisg.tapastasks;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
-import java.util.Collections;
-
@SpringBootApplication
public class TapasTasksApplication {
public static void main(String[] args) {
-
SpringApplication tapasTasksApp = new SpringApplication(TapasTasksApplication.class);
tapasTasksApp.run(args);
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java
new file mode 100644
index 0000000..94c1f47
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java
@@ -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
+ * JSON Patch 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 extractFirstTaskStatusChange() {
+ Optional 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 extractFirstServiceProviderChange() {
+ Optional 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 extractFirstOutputDataAddition() {
+ Optional 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 extractFirst(Predicate super JsonNode> predicate) {
+ Stream 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);
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java
new file mode 100644
index 0000000..eb89415
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java
@@ -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
+ * IANA 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);
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java
new file mode 100644
index 0000000..fbeb7b7
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java
@@ -0,0 +1,3 @@
+package ch.unisg.tapastasks.tasks.adapter.in.messaging;
+
+public class UnknownEventException extends RuntimeException { }
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java
new file mode 100644
index 0000000..4c26b80
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java
@@ -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 serviceProvider = representation.extractFirstServiceProviderChange();
+
+ TaskAssignedEvent taskAssignedEvent = new TaskAssignedEvent(new TaskId(taskId), serviceProvider);
+ TaskAssignedEventHandler taskAssignedEventHandler = new TaskAssignedHandler();
+
+ return taskAssignedEventHandler.handleTaskAssigned(taskAssignedEvent);
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java
new file mode 100644
index 0000000..940d6fa
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java
@@ -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
+ * JSON PATCH 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: http://jsonpatch.com/
+ * 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 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 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());
+ }
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java
new file mode 100644
index 0000000..8912968
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java
@@ -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;
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java
new file mode 100644
index 0000000..f1db541
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java
@@ -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 serviceProvider = representation.extractFirstServiceProviderChange();
+ Optional outputData = representation.extractFirstOutputDataAddition();
+
+ TaskExecutedEvent taskExecutedEvent = new TaskExecutedEvent(new Task.TaskId(taskId),
+ serviceProvider, outputData);
+ TaskExecutedEventHandler taskExecutedEventHandler = new TaskExecutedHandler();
+
+ return taskExecutedEventHandler.handleTaskExecuted(taskExecutedEvent);
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java
new file mode 100644
index 0000000..aa2f6b4
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java
@@ -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 serviceProvider = representation.extractFirstServiceProviderChange();
+
+ TaskStartedEvent taskStartedEvent = new TaskStartedEvent(new TaskId(taskId), serviceProvider);
+ TaskStartedEventHandler taskStartedEventHandler = new TaskStartedHandler();
+
+ return taskStartedEventHandler.handleTaskStarted(taskStartedEvent);
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java
index 53bebc1..234dcde 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java
@@ -1,8 +1,12 @@
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.AddNewTaskToTaskListUseCase;
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.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -12,29 +16,67 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
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
public class AddNewTaskToTaskListWebController {
private final AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase;
+ // Used to retrieve properties from application.properties
+ @Autowired
+ private Environment environment;
+
public AddNewTaskToTaskListWebController(AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase) {
this.addNewTaskToTaskListUseCase = addNewTaskToTaskListUseCase;
}
- @PostMapping(path = "/tasks/", consumes = {TaskMediaType.TASK_MEDIA_TYPE})
- public ResponseEntity addNewTaskTaskToTaskList(@RequestBody Task task) {
+ @PostMapping(path = "/tasks/", consumes = {TaskJsonRepresentation.MEDIA_TYPE})
+ public ResponseEntity addNewTaskTaskToTaskList(@RequestBody TaskJsonRepresentation payload) {
try {
- AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(
- task.getTaskName(), task.getTaskType()
- );
+ Task.TaskName taskName = new Task.TaskName(payload.getTaskName());
+ 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 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
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) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java
index 0eb6bea..d60e4d1 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java
@@ -1,8 +1,10 @@
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.RetrieveTaskFromTaskListUseCase;
import ch.unisg.tapastasks.tasks.domain.Task;
+import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -11,6 +13,11 @@ import org.springframework.web.server.ResponseStatusException;
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
public class RetrieveTaskFromTaskListWebController {
private final RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase;
@@ -19,10 +26,17 @@ public class RetrieveTaskFromTaskListWebController {
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}")
public ResponseEntity retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) {
- RetrieveTaskFromTaskListQuery command = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId));
- Optional updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(command);
+ RetrieveTaskFromTaskListQuery query = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId));
+ Optional updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(query);
// Check if the task with the given identifier exists
if (updatedTaskOpt.isEmpty()) {
@@ -30,11 +44,16 @@ public class RetrieveTaskFromTaskListWebController {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
- // Add the content type as a response header
- HttpHeaders responseHeaders = new HttpHeaders();
- responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE);
+ try {
+ String taskRepresentation = TaskJsonRepresentation.serialize(updatedTaskOpt.get());
- return new ResponseEntity<>(TaskMediaType.serialize(updatedTaskOpt.get()), responseHeaders,
- HttpStatus.OK);
+ // Add the content type as a response header
+ HttpHeaders responseHeaders = new HttpHeaders();
+ responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
+
+ return new ResponseEntity<>(taskRepresentation, responseHeaders, HttpStatus.OK);
+ } catch (JsonProcessingException e) {
+ throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
+ }
}
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java
deleted file mode 100644
index 3c555e5..0000000
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java
+++ /dev/null
@@ -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() { }
-}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java
new file mode 100644
index 0000000..7deb844
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java
@@ -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());
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java
new file mode 100644
index 0000000..ec21e8c
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java
@@ -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());
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java
new file mode 100644
index 0000000..758be0b
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java
@@ -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());
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java
index a0e0fec..fbb66ed 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java
@@ -1,23 +1,30 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
-import ch.unisg.tapastasks.tasks.domain.Task.TaskType;
-import ch.unisg.tapastasks.tasks.domain.Task.TaskName;
+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 AddNewTaskToTaskListCommand extends SelfValidating {
@NotNull
- private final TaskName taskName;
+ private final Task.TaskName taskName;
@NotNull
- private final TaskType taskType;
+ private final Task.TaskType taskType;
- public AddNewTaskToTaskListCommand(TaskName taskName, TaskType taskType) {
+ @Getter
+ private final Optional originalTaskUri;
+
+ public AddNewTaskToTaskListCommand(Task.TaskName taskName, Task.TaskType taskType,
+ Optional originalTaskUri) {
this.taskName = taskName;
this.taskType = taskType;
+ this.originalTaskUri = originalTaskUri;
+
this.validateSelf();
}
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java
index 40afc1d..cf7d787 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java
@@ -5,5 +5,5 @@ import ch.unisg.tapastasks.tasks.domain.Task;
import java.util.Optional;
public interface RetrieveTaskFromTaskListUseCase {
- Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command);
+ Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query);
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java
new file mode 100644
index 0000000..c58d034
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java
@@ -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 {
+ @NotNull
+ private final Task.TaskId taskId;
+
+ @Getter
+ private final Optional serviceProvider;
+
+ public TaskAssignedEvent(Task.TaskId taskId, Optional serviceProvider) {
+ this.taskId = taskId;
+ this.serviceProvider = serviceProvider;
+
+ this.validateSelf();
+ }
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java
new file mode 100644
index 0000000..67f78dd
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java
@@ -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);
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java
new file mode 100644
index 0000000..7ed9c84
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java
@@ -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 {
+ @NotNull
+ private final TaskId taskId;
+
+ @Getter
+ private final Optional serviceProvider;
+
+ @Getter
+ private final Optional outputData;
+
+ public TaskExecutedEvent(TaskId taskId, Optional serviceProvider,
+ Optional outputData) {
+ this.taskId = taskId;
+
+ this.serviceProvider = serviceProvider;
+ this.outputData = outputData;
+
+ this.validateSelf();
+ }
+
+
+
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java
new file mode 100644
index 0000000..c1a18dc
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java
@@ -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);
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java
new file mode 100644
index 0000000..8fad698
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java
@@ -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 {
+ @NotNull
+ private final Task.TaskId taskId;
+
+ @Getter
+ private final Optional serviceProvider;
+
+ public TaskStartedEvent(Task.TaskId taskId, Optional serviceProvider) {
+ this.taskId = taskId;
+ this.serviceProvider = serviceProvider;
+
+ this.validateSelf();
+ }
+
+
+
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java
new file mode 100644
index 0000000..0da730e
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java
@@ -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);
+}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java
index 48c75a6..2380fcf 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java
@@ -21,7 +21,13 @@ public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase
@Override
public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) {
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.
//This event should be considered as a light-weight "integration event" to communicate with other services.
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java
index 46043b0..fd6aea5 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java
@@ -15,8 +15,8 @@ import java.util.Optional;
@Transactional
public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase {
@Override
- public Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command) {
+ public Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query) {
TaskList taskList = TaskList.getTapasTaskList();
- return taskList.retrieveTaskById(command.getTaskId());
+ return taskList.retrieveTaskById(query.getTaskId());
}
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java
index 0dcafc3..b664a64 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java
@@ -8,7 +8,7 @@ import java.util.UUID;
/**This is a domain entity**/
public class Task {
- public enum State {
+ public enum Status {
OPEN, ASSIGNED, RUNNING, EXECUTED
}
@@ -22,38 +22,81 @@ public class Task {
private final TaskType taskType;
@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.taskType = taskType;
- this.taskState = new TaskState(State.OPEN);
- this.taskId = new TaskId(UUID.randomUUID().toString());
+ this.originalTaskUri = taskUri;
+
+ this.taskStatus = new TaskStatus(Status.OPEN);
+
+ this.inputData = null;
+ this.outputData = null;
}
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
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
public static class TaskId {
- private String value;
+ String value;
}
@Value
public static class TaskName {
- private String value;
- }
-
- @Value
- public static class TaskState {
- private State value;
+ String value;
}
@Value
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;
}
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java
index 2b90da5..4a52c62 100644
--- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java
@@ -3,7 +3,6 @@ package ch.unisg.tapastasks.tasks.domain;
import lombok.Getter;
import lombok.Value;
-import javax.swing.text.html.Option;
import java.util.LinkedList;
import java.util.List;
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
//--> 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 TaskList(TaskListName taskListName) {
@@ -35,14 +34,27 @@ public class TaskList {
//Only the aggregate root is allowed to create new tasks and add them to the task list.
//Note: Here we could add some sophisticated invariants/business rules that the aggregate root checks
public Task addNewTaskWithNameAndType(Task.TaskName name, Task.TaskType type) {
- Task newTask = Task.createTaskWithNameAndType(name,type);
- 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());
+ Task newTask = Task.createTaskWithNameAndType(name, type);
+ this.addNewTaskToList(newTask);
+
+ 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.
//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).
- 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 retrieveTaskById(Task.TaskId id) {
@@ -55,6 +67,43 @@ public class TaskList {
return Optional.empty();
}
+ public Task changeTaskStatusToAssigned(Task.TaskId id, Optional serviceProvider)
+ throws TaskNotFoundException {
+ return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty());
+ }
+
+ public Task changeTaskStatusToRunning(Task.TaskId id, Optional serviceProvider)
+ throws TaskNotFoundException {
+ return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty());
+ }
+
+ public Task changeTaskStatusToExecuted(Task.TaskId id, Optional serviceProvider,
+ Optional outputData) throws TaskNotFoundException {
+ return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData);
+ }
+
+ private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional serviceProvider,
+ Optional outputData) {
+ Optional 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
public static class TaskListName {
private String value;
@@ -64,5 +113,4 @@ public class TaskList {
public static class ListOfTasks {
private List value;
}
-
}
diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java
new file mode 100644
index 0000000..830b934
--- /dev/null
+++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java
@@ -0,0 +1,3 @@
+package ch.unisg.tapastasks.tasks.domain;
+
+public class TaskNotFoundException extends RuntimeException { }
diff --git a/tapas-tasks/src/main/resources/application.properties b/tapas-tasks/src/main/resources/application.properties
index 4d360de..fe25873 100644
--- a/tapas-tasks/src/main/resources/application.properties
+++ b/tapas-tasks/src/main/resources/application.properties
@@ -1 +1,2 @@
server.port=8081
+baseuri=https://tapas-tasks.86-119-34-23.nip.io/