Add Auction House; Extend uniform HTTP API for TAPAS-Tasks
This commit is contained in:
parent
ebb91a6010
commit
34fb3c682f
6
.github/workflows/build-and-deploy.yml
vendored
6
.github/workflows/build-and-deploy.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
9
tapas-auction-house/.editorconfig
Normal file
9
tapas-auction-house/.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
33
tapas-auction-house/.gitignore
vendored
Normal file
33
tapas-auction-house/.gitignore
vendored
Normal file
|
@ -0,0 +1,33 @@
|
|||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
117
tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
117
tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java
vendored
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright 2007-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import java.net.*;
|
||||
import java.io.*;
|
||||
import java.nio.channels.*;
|
||||
import java.util.Properties;
|
||||
|
||||
public class MavenWrapperDownloader {
|
||||
|
||||
private static final String WRAPPER_VERSION = "0.5.6";
|
||||
/**
|
||||
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
|
||||
*/
|
||||
private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
|
||||
+ WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
|
||||
|
||||
/**
|
||||
* Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
|
||||
* use instead of the default one.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
|
||||
".mvn/wrapper/maven-wrapper.properties";
|
||||
|
||||
/**
|
||||
* Path where the maven-wrapper.jar will be saved to.
|
||||
*/
|
||||
private static final String MAVEN_WRAPPER_JAR_PATH =
|
||||
".mvn/wrapper/maven-wrapper.jar";
|
||||
|
||||
/**
|
||||
* Name of the property which should be used to override the default download url for the wrapper.
|
||||
*/
|
||||
private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
|
||||
|
||||
public static void main(String args[]) {
|
||||
System.out.println("- Downloader started");
|
||||
File baseDirectory = new File(args[0]);
|
||||
System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
|
||||
|
||||
// If the maven-wrapper.properties exists, read it and check if it contains a custom
|
||||
// wrapperUrl parameter.
|
||||
File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
|
||||
String url = DEFAULT_DOWNLOAD_URL;
|
||||
if(mavenWrapperPropertyFile.exists()) {
|
||||
FileInputStream mavenWrapperPropertyFileInputStream = null;
|
||||
try {
|
||||
mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
|
||||
Properties mavenWrapperProperties = new Properties();
|
||||
mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
|
||||
url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
|
||||
} catch (IOException e) {
|
||||
System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
|
||||
} finally {
|
||||
try {
|
||||
if(mavenWrapperPropertyFileInputStream != null) {
|
||||
mavenWrapperPropertyFileInputStream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore ...
|
||||
}
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading from: " + url);
|
||||
|
||||
File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
|
||||
if(!outputFile.getParentFile().exists()) {
|
||||
if(!outputFile.getParentFile().mkdirs()) {
|
||||
System.out.println(
|
||||
"- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
|
||||
}
|
||||
}
|
||||
System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
|
||||
try {
|
||||
downloadFileFromURL(url, outputFile);
|
||||
System.out.println("Done");
|
||||
System.exit(0);
|
||||
} catch (Throwable e) {
|
||||
System.out.println("- Error downloading");
|
||||
e.printStackTrace();
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void downloadFileFromURL(String urlString, File destination) throws Exception {
|
||||
if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
|
||||
String username = System.getenv("MVNW_USERNAME");
|
||||
char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
|
||||
Authenticator.setDefault(new Authenticator() {
|
||||
@Override
|
||||
protected PasswordAuthentication getPasswordAuthentication() {
|
||||
return new PasswordAuthentication(username, password);
|
||||
}
|
||||
});
|
||||
}
|
||||
URL website = new URL(urlString);
|
||||
ReadableByteChannel rbc;
|
||||
rbc = Channels.newChannel(website.openStream());
|
||||
FileOutputStream fos = new FileOutputStream(destination);
|
||||
fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
|
||||
fos.close();
|
||||
rbc.close();
|
||||
}
|
||||
|
||||
}
|
BIN
tapas-auction-house/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
BIN
tapas-auction-house/.mvn/wrapper/maven-wrapper.jar
vendored
Normal file
Binary file not shown.
2
tapas-auction-house/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
2
tapas-auction-house/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
|
||||
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
|
101
tapas-auction-house/README.md
Normal file
101
tapas-auction-house/README.md
Normal file
|
@ -0,0 +1,101 @@
|
|||
# tapas-auction-house
|
||||
|
||||
The Auction House is the part of your TAPAS application that is largely responsible for the interactions
|
||||
with the TAPAS applications developed by the other groups. More precisely, it is responsible for
|
||||
launching and managing auctions and it is implemented following the Hexagonal Architecture (based on
|
||||
examples from book "Get Your Hands Dirty on Clean Architecture" by Tom Hombergs).
|
||||
|
||||
Technologies: Spring Boot, Maven
|
||||
|
||||
**Note:** this repository contains an [EditorConfig](https://editorconfig.org/) file (`.editorconfig`)
|
||||
with default editor settings. EditorConfig is supported out-of-the-box by the IntelliJ IDE. To help maintain
|
||||
consistent code styles, we recommend to reuse this editor configuration file in all your services.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This project provides a partial implementation of the Auction House. The code is documented in detail,
|
||||
here we only include a summary of implemented features:
|
||||
* running and managing auctions:
|
||||
* each auction has a deadline by which it is open for bids
|
||||
* once the deadline has passed, the auction house closes the auction and selects a random bid
|
||||
* starting an auction using a command via an HTTP adapter (see sample request below)
|
||||
* retrieving the list of open auctions via an HTTP adapter, i.e. auctions accepting bids (see sample
|
||||
request below)
|
||||
* receiving events when executors are added to the TAPAS application (both via HTTP and MQTT adapters)
|
||||
* the logic for automatic placement of bids in auctions: the auction house will place a bid in every
|
||||
auction for which there is at least one executor that can handle the type of task
|
||||
being auctioned
|
||||
* discovery of auction houses via a provided resource directory (see assignment sheet for
|
||||
Exercises 5 & 6 for more details)
|
||||
|
||||
## Overview of Adapters
|
||||
|
||||
In addition to the overall skeleton of the auction house, the current partial implementation provides
|
||||
several adapters to help you get started.
|
||||
|
||||
### HTTP Adapters
|
||||
|
||||
Sample HTTP request for launching an auction:
|
||||
|
||||
```shell
|
||||
curl -i --location --request POST 'http://localhost:8083/auctions/' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"taskUri" : "http://example.org",
|
||||
"taskType" : "taskType1",
|
||||
"deadline" : 10000
|
||||
}'
|
||||
|
||||
HTTP/1.1 201
|
||||
Content-Type: application/json
|
||||
Content-Length: 131
|
||||
Date: Sun, 17 Oct 2021 22:34:13 GMT
|
||||
|
||||
{
|
||||
"auctionId":"1",
|
||||
"auctionHouseUri":"http://localhost:8083/",
|
||||
"taskUri":"http://example.org",
|
||||
"taskType":"taskType1",
|
||||
"deadline":10000
|
||||
}
|
||||
```
|
||||
|
||||
Sample HTTP request for retrieving auctions currently open for bids:
|
||||
|
||||
```shell
|
||||
curl -i --location --request GET 'http://localhost:8083/auctions/'
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: application/json
|
||||
Content-Length: 133
|
||||
Date: Sun, 17 Oct 2021 22:34:20 GMT
|
||||
|
||||
[
|
||||
{
|
||||
"auctionId":"1",
|
||||
"auctionHouseUri":"http://localhost:8083/",
|
||||
"taskUri":"http://example.org",
|
||||
"taskType":"taskType1",
|
||||
"deadline":10000
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java)
|
||||
via an HTTP request:
|
||||
|
||||
```shell
|
||||
curl -i --location --request POST 'http://localhost:8083/executors/taskType1/executor1'
|
||||
|
||||
HTTP/1.1 204
|
||||
Date: Sun, 17 Oct 2021 22:38:45 GMT
|
||||
```
|
||||
|
||||
### MQTT Adapters
|
||||
|
||||
Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java)
|
||||
via an MQTT message via HiveMQ's [MQTT CLI](https://hivemq.github.io/mqtt-cli/):
|
||||
|
||||
```shell
|
||||
mqtt pub -t ch/unisg/tapas-group1/executors -m '{ "taskType" : "taskType1", "executorId" : "executor1" }'
|
||||
```
|
310
tapas-auction-house/mvnw
vendored
Executable file
310
tapas-auction-house/mvnw
vendored
Executable file
|
@ -0,0 +1,310 @@
|
|||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# https://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Maven Start Up Batch script
|
||||
#
|
||||
# Required ENV vars:
|
||||
# ------------------
|
||||
# JAVA_HOME - location of a JDK home dir
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# M2_HOME - location of maven2's installed home dir
|
||||
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
# e.g. to debug Maven itself, use
|
||||
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
if [ -z "$MAVEN_SKIP_RC" ] ; then
|
||||
|
||||
if [ -f /etc/mavenrc ] ; then
|
||||
. /etc/mavenrc
|
||||
fi
|
||||
|
||||
if [ -f "$HOME/.mavenrc" ] ; then
|
||||
. "$HOME/.mavenrc"
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# OS specific support. $var _must_ be set to either true or false.
|
||||
cygwin=false;
|
||||
darwin=false;
|
||||
mingw=false
|
||||
case "`uname`" in
|
||||
CYGWIN*) cygwin=true ;;
|
||||
MINGW*) mingw=true;;
|
||||
Darwin*) darwin=true
|
||||
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
|
||||
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
if [ -x "/usr/libexec/java_home" ]; then
|
||||
export JAVA_HOME="`/usr/libexec/java_home`"
|
||||
else
|
||||
export JAVA_HOME="/Library/Java/Home"
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
if [ -r /etc/gentoo-release ] ; then
|
||||
JAVA_HOME=`java-config --jre-home`
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$M2_HOME" ] ; then
|
||||
## resolve links - $0 may be a link to maven's home
|
||||
PRG="$0"
|
||||
|
||||
# need this for relative symlinks
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG="`dirname "$PRG"`/$link"
|
||||
fi
|
||||
done
|
||||
|
||||
saveddir=`pwd`
|
||||
|
||||
M2_HOME=`dirname "$PRG"`/..
|
||||
|
||||
# make it fully qualified
|
||||
M2_HOME=`cd "$M2_HOME" && pwd`
|
||||
|
||||
cd "$saveddir"
|
||||
# echo Using m2 at $M2_HOME
|
||||
fi
|
||||
|
||||
# For Cygwin, ensure paths are in UNIX format before anything is touched
|
||||
if $cygwin ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --unix "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
|
||||
fi
|
||||
|
||||
# For Mingw, ensure paths are in UNIX format before anything is touched
|
||||
if $mingw ; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME="`(cd "$M2_HOME"; pwd)`"
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ]; then
|
||||
javaExecutable="`which javac`"
|
||||
if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
|
||||
# readlink(1) is not available as standard on Solaris 10.
|
||||
readLink=`which readlink`
|
||||
if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
|
||||
if $darwin ; then
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
|
||||
else
|
||||
javaExecutable="`readlink -f \"$javaExecutable\"`"
|
||||
fi
|
||||
javaHome="`dirname \"$javaExecutable\"`"
|
||||
javaHome=`expr "$javaHome" : '\(.*\)/bin'`
|
||||
JAVA_HOME="$javaHome"
|
||||
export JAVA_HOME
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$JAVACMD" ] ; then
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
else
|
||||
JAVACMD="`which java`"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
echo "Error: JAVA_HOME is not defined correctly." >&2
|
||||
echo " We cannot execute $JAVACMD" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$JAVA_HOME" ] ; then
|
||||
echo "Warning: JAVA_HOME environment variable is not set."
|
||||
fi
|
||||
|
||||
CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
|
||||
|
||||
# traverses directory structure from process work directory to filesystem root
|
||||
# first directory with .mvn subdirectory is considered project base directory
|
||||
find_maven_basedir() {
|
||||
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Path not specified to find_maven_basedir"
|
||||
return 1
|
||||
fi
|
||||
|
||||
basedir="$1"
|
||||
wdir="$1"
|
||||
while [ "$wdir" != '/' ] ; do
|
||||
if [ -d "$wdir"/.mvn ] ; then
|
||||
basedir=$wdir
|
||||
break
|
||||
fi
|
||||
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
|
||||
if [ -d "${wdir}" ]; then
|
||||
wdir=`cd "$wdir/.."; pwd`
|
||||
fi
|
||||
# end of workaround
|
||||
done
|
||||
echo "${basedir}"
|
||||
}
|
||||
|
||||
# concatenates all lines of a file
|
||||
concat_lines() {
|
||||
if [ -f "$1" ]; then
|
||||
echo "$(tr -s '\n' ' ' < "$1")"
|
||||
fi
|
||||
}
|
||||
|
||||
BASE_DIR=`find_maven_basedir "$(pwd)"`
|
||||
if [ -z "$BASE_DIR" ]; then
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
##########################################################################################
|
||||
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
# This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
##########################################################################################
|
||||
if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found .mvn/wrapper/maven-wrapper.jar"
|
||||
fi
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
|
||||
fi
|
||||
if [ -n "$MVNW_REPOURL" ]; then
|
||||
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
else
|
||||
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
fi
|
||||
while IFS="=" read key value; do
|
||||
case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
|
||||
esac
|
||||
done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Downloading from: $jarUrl"
|
||||
fi
|
||||
wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
|
||||
if $cygwin; then
|
||||
wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
|
||||
fi
|
||||
|
||||
if command -v wget > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found wget ... using wget"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
wget "$jarUrl" -O "$wrapperJarPath"
|
||||
else
|
||||
wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
|
||||
fi
|
||||
elif command -v curl > /dev/null; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Found curl ... using curl"
|
||||
fi
|
||||
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
|
||||
curl -o "$wrapperJarPath" "$jarUrl" -f
|
||||
else
|
||||
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
|
||||
fi
|
||||
|
||||
else
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo "Falling back to using Java to download"
|
||||
fi
|
||||
javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
|
||||
# For Cygwin, switch paths to Windows format before running javac
|
||||
if $cygwin; then
|
||||
javaClass=`cygpath --path --windows "$javaClass"`
|
||||
fi
|
||||
if [ -e "$javaClass" ]; then
|
||||
if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Compiling MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
# Compiling the Java class
|
||||
("$JAVA_HOME/bin/javac" "$javaClass")
|
||||
fi
|
||||
if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
|
||||
# Running the downloader
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo " - Running MavenWrapperDownloader.java ..."
|
||||
fi
|
||||
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
##########################################################################################
|
||||
# End of extension
|
||||
##########################################################################################
|
||||
|
||||
export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
|
||||
if [ "$MVNW_VERBOSE" = true ]; then
|
||||
echo $MAVEN_PROJECTBASEDIR
|
||||
fi
|
||||
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin; then
|
||||
[ -n "$M2_HOME" ] &&
|
||||
M2_HOME=`cygpath --path --windows "$M2_HOME"`
|
||||
[ -n "$JAVA_HOME" ] &&
|
||||
JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
|
||||
[ -n "$CLASSPATH" ] &&
|
||||
CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
|
||||
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
|
||||
MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
|
||||
fi
|
||||
|
||||
# Provide a "standardized" way to retrieve the CLI args that will
|
||||
# work with both Windows and non-Windows executions.
|
||||
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
|
||||
export MAVEN_CMD_LINE_ARGS
|
||||
|
||||
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
exec "$JAVACMD" \
|
||||
$MAVEN_OPTS \
|
||||
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
|
||||
"-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
|
||||
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
|
182
tapas-auction-house/mvnw.cmd
vendored
Normal file
182
tapas-auction-house/mvnw.cmd
vendored
Normal file
|
@ -0,0 +1,182 @@
|
|||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM https://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Maven Start Up Batch script
|
||||
@REM
|
||||
@REM Required ENV vars:
|
||||
@REM JAVA_HOME - location of a JDK home dir
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM M2_HOME - location of maven2's installed home dir
|
||||
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
|
||||
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
|
||||
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
|
||||
@REM e.g. to debug Maven itself, use
|
||||
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
|
||||
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
|
||||
@echo off
|
||||
@REM set title of command window
|
||||
title %0
|
||||
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
|
||||
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
|
||||
|
||||
@REM set %HOME% to equivalent of $HOME
|
||||
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
|
||||
|
||||
@REM Execute a user defined script before this one
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
|
||||
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
|
||||
if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
|
||||
:skipRcPre
|
||||
|
||||
@setlocal
|
||||
|
||||
set ERROR_CODE=0
|
||||
|
||||
@REM To isolate internal variables from possible post scripts, we use another setlocal
|
||||
@setlocal
|
||||
|
||||
@REM ==== START VALIDATION ====
|
||||
if not "%JAVA_HOME%" == "" goto OkJHome
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME not found in your environment. >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
:OkJHome
|
||||
if exist "%JAVA_HOME%\bin\java.exe" goto init
|
||||
|
||||
echo.
|
||||
echo Error: JAVA_HOME is set to an invalid directory. >&2
|
||||
echo JAVA_HOME = "%JAVA_HOME%" >&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the >&2
|
||||
echo location of your Java installation. >&2
|
||||
echo.
|
||||
goto error
|
||||
|
||||
@REM ==== END VALIDATION ====
|
||||
|
||||
:init
|
||||
|
||||
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
|
||||
@REM Fallback to current working directory if not found.
|
||||
|
||||
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
|
||||
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
|
||||
|
||||
set EXEC_DIR=%CD%
|
||||
set WDIR=%EXEC_DIR%
|
||||
:findBaseDir
|
||||
IF EXIST "%WDIR%"\.mvn goto baseDirFound
|
||||
cd ..
|
||||
IF "%WDIR%"=="%CD%" goto baseDirNotFound
|
||||
set WDIR=%CD%
|
||||
goto findBaseDir
|
||||
|
||||
:baseDirFound
|
||||
set MAVEN_PROJECTBASEDIR=%WDIR%
|
||||
cd "%EXEC_DIR%"
|
||||
goto endDetectBaseDir
|
||||
|
||||
:baseDirNotFound
|
||||
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
|
||||
cd "%EXEC_DIR%"
|
||||
|
||||
:endDetectBaseDir
|
||||
|
||||
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
|
||||
|
||||
@setlocal EnableExtensions EnableDelayedExpansion
|
||||
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
|
||||
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
|
||||
|
||||
:endReadAdditionalConfig
|
||||
|
||||
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
|
||||
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
|
||||
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
|
||||
|
||||
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
|
||||
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
|
||||
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
|
||||
)
|
||||
|
||||
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
|
||||
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
|
||||
if exist %WRAPPER_JAR% (
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Found %WRAPPER_JAR%
|
||||
)
|
||||
) else (
|
||||
if not "%MVNW_REPOURL%" == "" (
|
||||
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
|
||||
)
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Couldn't find %WRAPPER_JAR%, downloading it ...
|
||||
echo Downloading from: %DOWNLOAD_URL%
|
||||
)
|
||||
|
||||
powershell -Command "&{"^
|
||||
"$webclient = new-object System.Net.WebClient;"^
|
||||
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
|
||||
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
|
||||
"}"^
|
||||
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
|
||||
"}"
|
||||
if "%MVNW_VERBOSE%" == "true" (
|
||||
echo Finished downloading %WRAPPER_JAR%
|
||||
)
|
||||
)
|
||||
@REM End of extension
|
||||
|
||||
@REM Provide a "standardized" way to retrieve the CLI args that will
|
||||
@REM work with both Windows and non-Windows executions.
|
||||
set MAVEN_CMD_LINE_ARGS=%*
|
||||
|
||||
%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
|
||||
if ERRORLEVEL 1 goto error
|
||||
goto end
|
||||
|
||||
:error
|
||||
set ERROR_CODE=1
|
||||
|
||||
:end
|
||||
@endlocal & set ERROR_CODE=%ERROR_CODE%
|
||||
|
||||
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
|
||||
@REM check for post script, once with legacy .bat ending and once with .cmd ending
|
||||
if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
|
||||
if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
|
||||
:skipRcPost
|
||||
|
||||
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
|
||||
if "%MAVEN_BATCH_PAUSE%" == "on" pause
|
||||
|
||||
if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
|
||||
|
||||
exit /B %ERROR_CODE%
|
80
tapas-auction-house/pom.xml
Normal file
80
tapas-auction-house/pom.xml
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.5.3</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>ch.unisg</groupId>
|
||||
<artifactId>tapas-auction-house</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>tapas-auction-house</name>
|
||||
<description>TAPAS Auction House</description>
|
||||
<properties>
|
||||
<java.version>11</java.version>
|
||||
</properties>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>Eclipse Paho Repo</id>
|
||||
<url>https://repo.eclipse.org/content/repositories/paho-releases/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.transaction</groupId>
|
||||
<artifactId>javax.transaction-api</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>javax.validation</groupId>
|
||||
<artifactId>validation-api</artifactId>
|
||||
<version>1.1.0.Final</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,71 @@
|
|||
package ch.unisg.tapas;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient;
|
||||
import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher;
|
||||
import ch.unisg.tapas.auctionhouse.adapter.common.clients.WebSubSubscriber;
|
||||
import ch.unisg.tapas.common.AuctionHouseResourceDirectory;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.eclipse.paho.client.mqttv3.MqttException;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Main TAPAS Auction House application.
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class TapasAuctionHouseApplication {
|
||||
private static final Logger LOGGER = LogManager.getLogger(TapasAuctionHouseApplication.class);
|
||||
|
||||
public static String RESOURCE_DIRECTORY = "https://api.interactions.ics.unisg.ch/auction-houses/";
|
||||
public static String MQTT_BROKER = "tcp://broker.hivemq.com:1883";
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class);
|
||||
|
||||
// We will use these bootstrap methods in Week 6:
|
||||
// bootstrapMarketplaceWithWebSub();
|
||||
// bootstrapMarketplaceWithMqtt();
|
||||
|
||||
tapasAuctioneerApp.run(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers auction houses and subscribes to WebSub notifications
|
||||
*/
|
||||
private static void bootstrapMarketplaceWithWebSub() {
|
||||
List<String> auctionHouseEndpoints = discoverAuctionHouseEndpoints();
|
||||
LOGGER.info("Found auction house endpoints: " + auctionHouseEndpoints);
|
||||
|
||||
WebSubSubscriber subscriber = new WebSubSubscriber();
|
||||
|
||||
for (String endpoint : auctionHouseEndpoints) {
|
||||
subscriber.subscribeToAuctionHouseEndpoint(URI.create(endpoint));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to an MQTT broker, presumably the one used by all TAPAS groups to communicate with
|
||||
* one another
|
||||
*/
|
||||
private static void bootstrapMarketplaceWithMqtt() {
|
||||
try {
|
||||
AuctionEventsMqttDispatcher dispatcher = new AuctionEventsMqttDispatcher();
|
||||
TapasMqttClient client = TapasMqttClient.getInstance(MQTT_BROKER, dispatcher);
|
||||
client.startReceivingMessages();
|
||||
} catch (MqttException e) {
|
||||
LOGGER.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<String> discoverAuctionHouseEndpoints() {
|
||||
AuctionHouseResourceDirectory rd = new AuctionHouseResourceDirectory(
|
||||
URI.create(RESOURCE_DIRECTORY)
|
||||
);
|
||||
|
||||
return rd.retrieveAuctionHouseEndpoints();
|
||||
}
|
||||
}
|
|
@ -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) { }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.application.handler.ExecutorAddedHandler;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent;
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/*
|
||||
THIS CLASS WILL BE PROVIDED ONLY AS A TEMPLATE; POINT OUT THE API NEEDS TO BE DEFINED
|
||||
*/
|
||||
|
||||
@RestController
|
||||
public class ExecutorAddedEventListenerHttpAdapter {
|
||||
|
||||
@PostMapping(path = "/executors/{taskType}/{executorId}")
|
||||
public ResponseEntity<String> handleExecutorAddedEvent(@PathVariable("taskType") String taskType,
|
||||
@PathVariable("executorId") String executorId) {
|
||||
|
||||
ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent(
|
||||
new ExecutorRegistry.ExecutorIdentifier(executorId),
|
||||
new Auction.AuctionedTaskType(taskType)
|
||||
);
|
||||
|
||||
ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler();
|
||||
newExecutorHandler.handleNewExecutorEvent(executorAddedEvent);
|
||||
|
||||
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt;
|
||||
|
||||
import org.eclipse.paho.client.mqttv3.*;
|
||||
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Dispatches MQTT messages for known topics to associated event listeners. Used in conjunction with
|
||||
* {@link ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient}.
|
||||
*
|
||||
* This is where you would define MQTT topics and map them to event listeners (see
|
||||
* {@link AuctionEventsMqttDispatcher#initRouter()}).
|
||||
*
|
||||
* This class is only provided as an example to help you bootstrap the project. You are welcomed to
|
||||
* change this class as you see fit.
|
||||
*/
|
||||
public class AuctionEventsMqttDispatcher {
|
||||
private final Map<String, AuctionEventMqttListener> router;
|
||||
|
||||
public AuctionEventsMqttDispatcher() {
|
||||
this.router = new Hashtable<>();
|
||||
initRouter();
|
||||
}
|
||||
|
||||
// TODO: Register here your topics and event listener adapters
|
||||
private void initRouter() {
|
||||
router.put("ch/unisg/tapas-group-tutors/executors", new ExecutorAddedEventListenerMqttAdapter());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all topics registered with this dispatcher.
|
||||
*
|
||||
* @return the set of registered topics
|
||||
*/
|
||||
public Set<String> getAllTopics() {
|
||||
return router.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches an event received via MQTT for a given topic.
|
||||
*
|
||||
* @param topic the topic for which the MQTT message was received
|
||||
* @param message the received MQTT message
|
||||
*/
|
||||
public void dispatchEvent(String topic, MqttMessage message) {
|
||||
AuctionEventMqttListener listener = router.get(topic);
|
||||
listener.handleEvent(message);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package ch.unisg.tapas.auctionhouse.adapter.in.web;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase;
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* Controller that handles HTTP requests for launching auctions. This controller implements the
|
||||
* {@link LaunchAuctionUseCase} use case using the {@link LaunchAuctionCommand}.
|
||||
*/
|
||||
@RestController
|
||||
public class LaunchAuctionWebController {
|
||||
private final LaunchAuctionUseCase launchAuctionUseCase;
|
||||
|
||||
/**
|
||||
* Constructs the controller.
|
||||
*
|
||||
* @param launchAuctionUseCase an implementation of the launch auction use case
|
||||
*/
|
||||
public LaunchAuctionWebController(LaunchAuctionUseCase launchAuctionUseCase) {
|
||||
this.launchAuctionUseCase = launchAuctionUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles HTTP POST requests for launching auctions. Note: you are free to modify this handler
|
||||
* as you see fit to reflect the discussions for the uniform HTTP API for the auction house.
|
||||
* You should also ensure that this handler has the exact behavior you would expect from the
|
||||
* defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.)
|
||||
*
|
||||
* @param payload a representation of the auction to be launched
|
||||
* @return
|
||||
*/
|
||||
@PostMapping(path = "/auctions/", consumes = AuctionJsonRepresentation.MEDIA_TYPE)
|
||||
public ResponseEntity<String> launchAuction(@RequestBody AuctionJsonRepresentation payload) {
|
||||
Auction.AuctionDeadline deadline = (payload.getDeadline() == null) ?
|
||||
null : new Auction.AuctionDeadline(payload.getDeadline());
|
||||
|
||||
LaunchAuctionCommand command = new LaunchAuctionCommand(
|
||||
new Auction.AuctionedTaskUri(URI.create(payload.getTaskUri())),
|
||||
new Auction.AuctionedTaskType(payload.getTaskType()),
|
||||
deadline
|
||||
);
|
||||
|
||||
// This command returns the created auction. We need the created auction to be able to
|
||||
// include a representation of it in the HTTP response.
|
||||
Auction auction = launchAuctionUseCase.launchAuction(command);
|
||||
|
||||
try {
|
||||
AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction);
|
||||
String auctionJson = AuctionJsonRepresentation.serialize(auction);
|
||||
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.add(HttpHeaders.CONTENT_TYPE, AuctionJsonRepresentation.MEDIA_TYPE);
|
||||
|
||||
// Return a 201 Created status code and a representation of the created auction
|
||||
return new ResponseEntity<>(auctionJson, responseHeaders, HttpStatus.CREATED);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package ch.unisg.tapas.auctionhouse.adapter.in.web;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase;
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Controller that handles HTTP requests for retrieving auctions hosted by this auction house that
|
||||
* are open for bids. This controller implements the {@link RetrieveOpenAuctionsUseCase} use case
|
||||
* using the {@link RetrieveOpenAuctionsQuery}.
|
||||
*/
|
||||
@RestController
|
||||
public class RetrieveOpenAuctionsWebController {
|
||||
private final RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase;
|
||||
|
||||
public RetrieveOpenAuctionsWebController(RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase) {
|
||||
this.retrieveAuctionListUseCase = retrieveAuctionListUseCase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles HTTP GET requests to retrieve the auctions that are open. Note: you are free to modify
|
||||
* this handler as you see fit to reflect the discussions for the uniform HTTP API for the
|
||||
* auction house. You should also ensure that this handler has the exact behavior you would expect
|
||||
* from the defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.).
|
||||
*
|
||||
* @return a representation of a collection with the auctions that are open for bids
|
||||
*/
|
||||
@GetMapping(path = "/auctions/")
|
||||
public ResponseEntity<String> retrieveOpenAuctions() {
|
||||
Collection<Auction> auctions =
|
||||
retrieveAuctionListUseCase.retrieveAuctions(new RetrieveOpenAuctionsQuery());
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
ArrayNode array = mapper.createArrayNode();
|
||||
|
||||
for (Auction auction : auctions) {
|
||||
AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction);
|
||||
JsonNode node = mapper.valueToTree(representation);
|
||||
array.add(node);
|
||||
}
|
||||
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json");
|
||||
|
||||
// TODO before providing to students: remove hub links
|
||||
responseHeaders.add(HttpHeaders.LINK, "<https://pubsubhubbub.appspot.com/>; rel=\"hub\"");
|
||||
responseHeaders.add(HttpHeaders.LINK, "<http://example.org/auctions/>; rel=\"self\"");
|
||||
|
||||
return new ResponseEntity<>(array.toString(), responseHeaders, HttpStatus.OK);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
import ch.unisg.tapas.common.SelfValidating;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Event that notifies this auction house that an auction was started by another auction house.
|
||||
*/
|
||||
@Value
|
||||
public class AuctionStartedEvent extends SelfValidating<AuctionStartedEvent> {
|
||||
@NotNull
|
||||
private final Auction auction;
|
||||
|
||||
public AuctionStartedEvent(Auction auction) {
|
||||
this.auction = auction;
|
||||
this.validateSelf();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
public interface AuctionStartedEventHandler {
|
||||
|
||||
boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskType;
|
||||
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier;
|
||||
import ch.unisg.tapas.common.SelfValidating;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Event that notifies the auction house that an executor has been added to this TAPAS application.
|
||||
*/
|
||||
@Value
|
||||
public class ExecutorAddedEvent extends SelfValidating<ExecutorAddedEvent> {
|
||||
@NotNull
|
||||
private final ExecutorIdentifier executorId;
|
||||
|
||||
@NotNull
|
||||
private final AuctionedTaskType taskType;
|
||||
|
||||
/**
|
||||
* Constructs an executor added event.
|
||||
*
|
||||
* @param executorId the identifier of the executor that was added to this TAPAS application
|
||||
*/
|
||||
public ExecutorAddedEvent(ExecutorIdentifier executorId, AuctionedTaskType taskType) {
|
||||
this.executorId = executorId;
|
||||
this.taskType = taskType;
|
||||
|
||||
this.validateSelf();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
public interface ExecutorAddedEventHandler {
|
||||
|
||||
boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier;
|
||||
import ch.unisg.tapas.common.SelfValidating;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Event that notifies the auction house that an executor has been removed from this TAPAS application.
|
||||
*/
|
||||
@Value
|
||||
public class ExecutorRemovedEvent extends SelfValidating<ExecutorRemovedEvent> {
|
||||
@NotNull
|
||||
private final ExecutorIdentifier executorId;
|
||||
|
||||
/**
|
||||
* Constructs an executor removed event.
|
||||
*
|
||||
* @param executorId the identifier of the executor that was removed from this TAPAS application
|
||||
*/
|
||||
public ExecutorRemovedEvent(ExecutorIdentifier executorId) {
|
||||
this.executorId = executorId;
|
||||
this.validateSelf();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
public interface ExecutorRemovedEventHandler {
|
||||
|
||||
boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent);
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
import ch.unisg.tapas.common.SelfValidating;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Command for launching an auction in this auction house.
|
||||
*/
|
||||
@Value
|
||||
public class LaunchAuctionCommand extends SelfValidating<LaunchAuctionCommand> {
|
||||
@NotNull
|
||||
private final Auction.AuctionedTaskUri taskUri;
|
||||
|
||||
@NotNull
|
||||
private final Auction.AuctionedTaskType taskType;
|
||||
|
||||
private final Auction.AuctionDeadline deadline;
|
||||
|
||||
/**
|
||||
* Constructs the launch action command.
|
||||
*
|
||||
* @param taskUri the URI of the auctioned task
|
||||
* @param taskType the type of the auctioned task
|
||||
* @param deadline the deadline by which the auction should receive bids (can be null if none)
|
||||
*/
|
||||
public LaunchAuctionCommand(Auction.AuctionedTaskUri taskUri, Auction.AuctionedTaskType taskType,
|
||||
Auction.AuctionDeadline deadline) {
|
||||
this.taskUri = taskUri;
|
||||
this.taskType = taskType;
|
||||
this.deadline = deadline;
|
||||
|
||||
this.validateSelf();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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 { }
|
|
@ -0,0 +1,10 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.in;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface RetrieveOpenAuctionsUseCase {
|
||||
|
||||
Collection<Auction> retrieveAuctions(RetrieveOpenAuctionsQuery query);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.out;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
import ch.unisg.tapas.auctionhouse.domain.Bid;
|
||||
import ch.unisg.tapas.common.SelfValidating;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* Command to place a bid for a given auction.
|
||||
*/
|
||||
@Value
|
||||
public class PlaceBidForAuctionCommand extends SelfValidating<PlaceBidForAuctionCommand> {
|
||||
@NotNull
|
||||
private final Auction auction;
|
||||
@NotNull
|
||||
private final Bid bid;
|
||||
|
||||
public PlaceBidForAuctionCommand(Auction auction, Bid bid) {
|
||||
this.auction = auction;
|
||||
this.bid = bid;
|
||||
this.validateSelf();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.port.out;
|
||||
|
||||
public interface PlaceBidForAuctionCommandPort {
|
||||
|
||||
void placeBid(PlaceBidForAuctionCommand command);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.service;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase;
|
||||
import ch.unisg.tapas.auctionhouse.domain.Auction;
|
||||
import ch.unisg.tapas.auctionhouse.domain.AuctionRegistry;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Service that implements {@link RetrieveOpenAuctionsUseCase} to retrieve all auctions in this auction
|
||||
* house that are open for bids.
|
||||
*/
|
||||
@Component
|
||||
public class RetrieveOpenAuctionsService implements RetrieveOpenAuctionsUseCase {
|
||||
|
||||
@Override
|
||||
public Collection<Auction> retrieveAuctions(RetrieveOpenAuctionsQuery query) {
|
||||
return AuctionRegistry.getInstance().getOpenAuctions();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package ch.unisg.tapas.auctionhouse.application.service;
|
||||
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort;
|
||||
import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort;
|
||||
import ch.unisg.tapas.auctionhouse.domain.*;
|
||||
import ch.unisg.tapas.common.ConfigProperties;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Service that implements the {@link LaunchAuctionUseCase} to start an auction. If a deadline is
|
||||
* specified for the auction, the service automatically closes the auction at the deadline. If a
|
||||
* deadline is not specified, the service closes the auction after 10s by default.
|
||||
*/
|
||||
@Component
|
||||
public class StartAuctionService implements LaunchAuctionUseCase {
|
||||
private static final Logger LOGGER = LogManager.getLogger(StartAuctionService.class);
|
||||
|
||||
private final static int DEFAULT_AUCTION_DEADLINE_MILLIS = 10000;
|
||||
|
||||
// Event port used to publish an auction started event
|
||||
private final AuctionStartedEventPort auctionStartedEventPort;
|
||||
// Event port used to publish an auction won event
|
||||
private final AuctionWonEventPort auctionWonEventPort;
|
||||
|
||||
private final ScheduledExecutorService service;
|
||||
private final AuctionRegistry auctions;
|
||||
|
||||
@Autowired
|
||||
private ConfigProperties config;
|
||||
|
||||
public StartAuctionService(AuctionStartedEventPort auctionStartedEventPort,
|
||||
AuctionWonEventPort auctionWonEventPort) {
|
||||
this.auctionStartedEventPort = auctionStartedEventPort;
|
||||
this.auctionWonEventPort = auctionWonEventPort;
|
||||
this.auctions = AuctionRegistry.getInstance();
|
||||
this.service = Executors.newScheduledThreadPool(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches an auction.
|
||||
*
|
||||
* @param command the domain command used to launch the auction (see {@link LaunchAuctionCommand})
|
||||
* @return the launched auction
|
||||
*/
|
||||
@Override
|
||||
public Auction launchAuction(LaunchAuctionCommand command) {
|
||||
Auction.AuctionDeadline deadline = (command.getDeadline() == null) ?
|
||||
new Auction.AuctionDeadline(DEFAULT_AUCTION_DEADLINE_MILLIS) : command.getDeadline();
|
||||
|
||||
// Create a new auction and add it to the auction registry
|
||||
Auction auction = new Auction(new Auction.AuctionHouseUri(config.getAuctionHouseUri()),
|
||||
command.getTaskUri(), command.getTaskType(), deadline);
|
||||
auctions.addAuction(auction);
|
||||
|
||||
// Schedule the closing of the auction at the deadline
|
||||
service.schedule(new CloseAuctionTask(auction.getAuctionId()), deadline.getValue(),
|
||||
TimeUnit.MILLISECONDS);
|
||||
|
||||
// Publish an auction started event
|
||||
AuctionStartedEvent auctionStartedEvent = new AuctionStartedEvent(auction);
|
||||
auctionStartedEventPort.publishAuctionStartedEvent(auctionStartedEvent);
|
||||
|
||||
return auction;
|
||||
}
|
||||
|
||||
/**
|
||||
* This task closes the auction at the deadline and selects a winner if any bids were placed. It
|
||||
* also sends out associated events and commands.
|
||||
*/
|
||||
private class CloseAuctionTask implements Runnable {
|
||||
Auction.AuctionId auctionId;
|
||||
|
||||
public CloseAuctionTask(Auction.AuctionId auctionId) {
|
||||
this.auctionId = auctionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Optional<Auction> auctionOpt = auctions.getAuctionById(auctionId);
|
||||
|
||||
if (auctionOpt.isPresent()) {
|
||||
Auction auction = auctionOpt.get();
|
||||
Optional<Bid> bid = auction.selectBid();
|
||||
|
||||
// Close the auction
|
||||
auction.close();
|
||||
|
||||
if (bid.isPresent()) {
|
||||
// Notify the bidder
|
||||
Bid.BidderName bidderName = bid.get().getBidderName();
|
||||
LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " for task "
|
||||
+ auction.getTaskUri().getValue() + " won by " + bidderName.getValue());
|
||||
|
||||
// Send an auction won event for the winning bid
|
||||
auctionWonEventPort.publishAuctionWonEvent(new AuctionWonEvent(bid.get()));
|
||||
} else {
|
||||
LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " ended with no bids for task "
|
||||
+ auction.getTaskUri().getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package ch.unisg.tapas.auctionhouse.domain;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Domain entity that models an auction.
|
||||
*/
|
||||
public class Auction {
|
||||
// Auctions have two possible states:
|
||||
// - open: waiting for bids
|
||||
// - closed: the auction deadline has expired, there may or may not be a winning bid
|
||||
public enum Status {
|
||||
OPEN, CLOSED
|
||||
}
|
||||
|
||||
// One way to generate auction identifiers is incremental starting from 1. This makes identifiers
|
||||
// predictable, which can help with debugging when multiple parties are interacting, but it also
|
||||
// means that auction identifiers are not universally unique unless they are part of a URI.
|
||||
// An alternative would be to use UUIDs (see constructor).
|
||||
private static long AUCTION_COUNTER = 1;
|
||||
|
||||
@Getter
|
||||
private AuctionId auctionId;
|
||||
|
||||
@Getter
|
||||
private AuctionStatus auctionStatus;
|
||||
|
||||
// URI that identifies the auction house that started this auction. Given a uniform, standard
|
||||
// HTTP API for auction houses, this URI can then be used as a base URI for interacting with
|
||||
// the identified auction house.
|
||||
@Getter
|
||||
private final AuctionHouseUri auctionHouseUri;
|
||||
|
||||
// URI that identifies the task for which the auction was launched. URIs are uniform identifiers
|
||||
// and can be referenced independent of context: because we have defined a uniform HTTP API for
|
||||
// TAPAS-Tasks, we can dereference this URI to retrieve a complete representation of the
|
||||
// auctioned task.
|
||||
@Getter
|
||||
private final AuctionedTaskUri taskUri;
|
||||
|
||||
// The type of the task being auctioned. We could also retrieve the task type by dereferencing
|
||||
// the task's URI, but given that the bidding is defined primarily based on task types, knowing
|
||||
// the task type avoids an additional HTTP request.
|
||||
@Getter
|
||||
private final AuctionedTaskType taskType;
|
||||
|
||||
// The deadline by which bids can be placed. Once the deadline expires, the auction is closed.
|
||||
@Getter
|
||||
private final AuctionDeadline deadline;
|
||||
|
||||
// Available bids.
|
||||
@Getter
|
||||
private final List<Bid> bids;
|
||||
|
||||
/**
|
||||
* Constructs an auction.
|
||||
*
|
||||
* @param auctionHouseUri the URI of the auction hause that started the auction
|
||||
* @param taskUri the URI of the task being auctioned
|
||||
* @param taskType the type of the task being auctioned
|
||||
* @param deadline the deadline by which the auction is open for bids
|
||||
*/
|
||||
public Auction(AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri,
|
||||
AuctionedTaskType taskType, AuctionDeadline deadline) {
|
||||
// Generates an incremental identifier
|
||||
this.auctionId = new AuctionId("" + AUCTION_COUNTER ++);
|
||||
// As an alternative, we could also generate an UUID
|
||||
// this.auctionId = new AuctionId(UUID.randomUUID().toString());
|
||||
|
||||
this.auctionStatus = new AuctionStatus(Status.OPEN);
|
||||
|
||||
this.auctionHouseUri = auctionHouseUri;
|
||||
this.taskUri = taskUri;
|
||||
this.taskType = taskType;
|
||||
|
||||
this.deadline = deadline;
|
||||
this.bids = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an auction.
|
||||
*
|
||||
* @param auctionId the identifier of the auction
|
||||
* @param auctionHouseUri the URI of the auction hause that started the auction
|
||||
* @param taskUri the URI of the task being auctioned
|
||||
* @param taskType the type of the task being auctioned
|
||||
* @param deadline the deadline by which the auction is open for bids
|
||||
*/
|
||||
public Auction(AuctionId auctionId, AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri,
|
||||
AuctionedTaskType taskType, AuctionDeadline deadline) {
|
||||
this(auctionHouseUri, taskUri, taskType, deadline);
|
||||
this.auctionId = auctionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a bid for this auction.
|
||||
*
|
||||
* @param bid the bid
|
||||
*/
|
||||
public void addBid(Bid bid) {
|
||||
bids.add(bid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects a bid randomly from the bids available for this auction.
|
||||
*
|
||||
* @return a winning bid or Optional.empty if no bid was made in this auction.
|
||||
*/
|
||||
public Optional<Bid> selectBid() {
|
||||
if (bids.isEmpty()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
int index = new Random().nextInt(bids.size());
|
||||
return Optional.of(bids.get(index));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the auction is open for bids.
|
||||
*
|
||||
* @return true if open for bids, false if the auction is closed
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return auctionStatus.getValue() == Status.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the auction. Called by the StartAuctionService after the auction deadline has expired.
|
||||
*/
|
||||
public void close() {
|
||||
auctionStatus = new AuctionStatus(Status.CLOSED);
|
||||
}
|
||||
|
||||
/*
|
||||
* Definitions of Value Objects
|
||||
*/
|
||||
|
||||
@Value
|
||||
public static class AuctionId {
|
||||
String value;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class AuctionStatus {
|
||||
Status value;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class AuctionHouseUri {
|
||||
URI value;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class AuctionedTaskUri {
|
||||
URI value;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class AuctionedTaskType {
|
||||
String value;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class AuctionDeadline {
|
||||
int value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package ch.unisg.tapas.auctionhouse.domain;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Registry that keeps an in-memory history of auctions (both open for bids and closed). This class
|
||||
* is a singleton. See also {@link Auction}.
|
||||
*/
|
||||
public class AuctionRegistry {
|
||||
private static AuctionRegistry registry;
|
||||
|
||||
private final Map<Auction.AuctionId, Auction> auctions;
|
||||
|
||||
private AuctionRegistry() {
|
||||
this.auctions = new Hashtable<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a reference to the auction registry.
|
||||
*
|
||||
* @return the auction registry
|
||||
*/
|
||||
public static synchronized AuctionRegistry getInstance() {
|
||||
if (registry == null) {
|
||||
registry = new AuctionRegistry();
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new auction to the registry
|
||||
* @param auction the new auction
|
||||
*/
|
||||
public void addAuction(Auction auction) {
|
||||
auctions.put(auction.getAuctionId(), auction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Places a bid. See also {@link Bid}.
|
||||
*
|
||||
* @param bid the bid to be placed.
|
||||
* @return false if the bid is for an auction with an unknown identifier, true otherwise
|
||||
*/
|
||||
public boolean placeBid(Bid bid) {
|
||||
if (!containsAuctionWithId(bid.getAuctionId())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Auction auction = getAuctionById(bid.getAuctionId()).get();
|
||||
auction.addBid(bid);
|
||||
auctions.put(bid.getAuctionId(), auction);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the registry contains an auction with the given identifier.
|
||||
*
|
||||
* @param auctionId the auction's identifier
|
||||
* @return true if the registry contains an auction with the given identifier, false otherwise
|
||||
*/
|
||||
public boolean containsAuctionWithId(Auction.AuctionId auctionId) {
|
||||
return auctions.containsKey(auctionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the auction with the given identifier if it exists.
|
||||
*
|
||||
* @param auctionId the auction's identifier
|
||||
* @return the auction or Optional.empty if the identifier is unknown
|
||||
*/
|
||||
public Optional<Auction> getAuctionById(Auction.AuctionId auctionId) {
|
||||
if (containsAuctionWithId(auctionId)) {
|
||||
return Optional.of(auctions.get(auctionId));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all auctions in the registry.
|
||||
*
|
||||
* @return a collection with all auctions
|
||||
*/
|
||||
public Collection<Auction> getAllAuctions() {
|
||||
return auctions.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves only the auctions that are open for bids.
|
||||
*
|
||||
* @return a collection with all open auctions
|
||||
*/
|
||||
public Collection<Auction> getOpenAuctions() {
|
||||
return getAllAuctions()
|
||||
.stream()
|
||||
.filter(auction -> auction.isOpen())
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package ch.unisg.tapas.auctionhouse.domain;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Registry that keeps a track of executors internal to the TAPAS application and the types of tasks
|
||||
* they can achieve. One executor may correspond to multiple task types. This mapping is used when
|
||||
* bidding for tasks: the auction house will only bid for tasks for which there is a known executor.
|
||||
* This class is a singleton.
|
||||
*/
|
||||
public class ExecutorRegistry {
|
||||
private static ExecutorRegistry registry;
|
||||
|
||||
private final Map<Auction.AuctionedTaskType, Set<ExecutorIdentifier>> executors;
|
||||
|
||||
private ExecutorRegistry() {
|
||||
this.executors = new Hashtable<>();
|
||||
}
|
||||
|
||||
public static synchronized ExecutorRegistry getInstance() {
|
||||
if (registry == null) {
|
||||
registry = new ExecutorRegistry();
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an executor to the registry for a given task type.
|
||||
*
|
||||
* @param taskType the type of the task
|
||||
* @param executorIdentifier the identifier of the executor (can be any string)
|
||||
* @return true unless a runtime exception occurs
|
||||
*/
|
||||
public boolean addExecutor(Auction.AuctionedTaskType taskType, ExecutorIdentifier executorIdentifier) {
|
||||
Set<ExecutorIdentifier> taskTypeExecs = executors.getOrDefault(taskType,
|
||||
Collections.synchronizedSet(new HashSet<>()));
|
||||
|
||||
taskTypeExecs.add(executorIdentifier);
|
||||
executors.put(taskType, taskTypeExecs);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an executor from the registry. The executor is disassociated from all known task types.
|
||||
*
|
||||
* @param executorIdentifier the identifier of the executor (can be any string)
|
||||
* @return true unless a runtime exception occurs
|
||||
*/
|
||||
public boolean removeExecutor(ExecutorIdentifier executorIdentifier) {
|
||||
Iterator<Auction.AuctionedTaskType> iterator = executors.keySet().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Auction.AuctionedTaskType taskType = iterator.next();
|
||||
Set<ExecutorIdentifier> set = executors.get(taskType);
|
||||
|
||||
set.remove(executorIdentifier);
|
||||
|
||||
if (set.isEmpty()) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the registry contains an executor for a given task type. Used during an auction to
|
||||
* decide if a bid should be placed.
|
||||
*
|
||||
* @param taskType the task type being auctioned
|
||||
* @return
|
||||
*/
|
||||
public boolean containsTaskType(Auction.AuctionedTaskType taskType) {
|
||||
return executors.containsKey(taskType);
|
||||
}
|
||||
|
||||
// Value Object for the executor identifier
|
||||
@Value
|
||||
public static class ExecutorIdentifier {
|
||||
String value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package ch.unisg.tapas.common;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class that wraps up the resource directory used to discover auction houses in Week 6.
|
||||
*/
|
||||
public class AuctionHouseResourceDirectory {
|
||||
private final URI rdEndpoint;
|
||||
|
||||
/**
|
||||
* Constructs a resource directory for auction house given a known URI.
|
||||
*
|
||||
* @param rdEndpoint the based endpoint of the resource directory
|
||||
*/
|
||||
public AuctionHouseResourceDirectory(URI rdEndpoint) {
|
||||
this.rdEndpoint = rdEndpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the endpoints of all auctions houses registered with this directory.
|
||||
* @return
|
||||
*/
|
||||
public List<String> retrieveAuctionHouseEndpoints() {
|
||||
List<String> auctionHouseEndpoints = new ArrayList<>();
|
||||
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(rdEndpoint).GET().build();
|
||||
|
||||
HttpResponse<String> response = HttpClient.newBuilder().build()
|
||||
.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
// For simplicity, here we just hard code the current representation used by our
|
||||
// resource directory for auction houses
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
JsonNode payload = objectMapper.readTree(response.body());
|
||||
|
||||
for (JsonNode node : payload) {
|
||||
auctionHouseEndpoints.add(node.get("endpoint").asText());
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return auctionHouseEndpoints;
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ch.unisg.tapas.common;
|
||||
|
||||
import javax.validation.*;
|
||||
import java.util.Set;
|
||||
|
||||
public class SelfValidating<T> {
|
||||
|
||||
private Validator validator;
|
||||
|
||||
public SelfValidating() {
|
||||
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
|
||||
validator = factory.getValidator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates all Bean Validations on the attributes of this
|
||||
* instance.
|
||||
*/
|
||||
protected void validateSelf() {
|
||||
Set<ConstraintViolation<T>> violations = validator.validate((T) this);
|
||||
if (!violations.isEmpty()) {
|
||||
throw new ConstraintViolationException(violations);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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/
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
|
@ -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/<task-UUID>` endpoint.
|
||||
The representation of a task is retrieved via an `HTTP GET` request to the URI of task.
|
||||
|
||||
A sample HTTP request with `curl`:
|
||||
```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.
|
||||
|
|
|
@ -16,6 +16,12 @@
|
|||
<properties>
|
||||
<java.version>11</java.version>
|
||||
</properties>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>Eclipse Paho Repo</id>
|
||||
<url>https://repo.eclipse.org/content/repositories/paho-releases/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
@ -49,17 +55,16 @@
|
|||
<version>1.1.0.Final</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.json</groupId>
|
||||
<artifactId>json</artifactId>
|
||||
<version>20210307</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.java-json-tools</groupId>
|
||||
<artifactId>json-patch</artifactId>
|
||||
<version>1.13</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>1.2.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package ch.unisg.tapastasks.tasks.adapter.in.formats;
|
||||
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
/**
|
||||
* This class is used to process JSON Patch operations for tasks: given a
|
||||
* <a href="http://jsonpatch.com/">JSON Patch</a> for updating the representational state of a task,
|
||||
* this class provides methods for extracting various operations of interest for our domain (e.g.,
|
||||
* changing the status of a task).
|
||||
*/
|
||||
public class TaskJsonPatchRepresentation {
|
||||
public static final String MEDIA_TYPE = "application/json-patch+json";
|
||||
|
||||
private final JsonNode patch;
|
||||
|
||||
/**
|
||||
* Constructs the JSON Patch representation.
|
||||
*
|
||||
* @param patch a JSON Patch as JsonNode
|
||||
*/
|
||||
public TaskJsonPatchRepresentation(JsonNode patch) {
|
||||
this.patch = patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the first task status replaced in this patch.
|
||||
*
|
||||
* @return the first task status changed in this patch or an empty {@link Optional} if none is
|
||||
* found
|
||||
*/
|
||||
public Optional<Task.Status> extractFirstTaskStatusChange() {
|
||||
Optional<JsonNode> status = extractFirst(node ->
|
||||
isPatchReplaceOperation(node) && hasPath(node, "/taskStatus")
|
||||
);
|
||||
|
||||
if (status.isPresent()) {
|
||||
String taskStatus = status.get().get("value").asText();
|
||||
return Optional.of(Task.Status.valueOf(taskStatus));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the first service provider added or replaced in this patch.
|
||||
*
|
||||
* @return the first service provider changed in this patch or an empty {@link Optional} if none
|
||||
* is found
|
||||
*/
|
||||
public Optional<Task.ServiceProvider> extractFirstServiceProviderChange() {
|
||||
Optional<JsonNode> serviceProvider = extractFirst(node ->
|
||||
(isPatchReplaceOperation(node) || isPatchAddOperation(node))
|
||||
&& hasPath(node, "/serviceProvider")
|
||||
);
|
||||
|
||||
return (serviceProvider.isEmpty()) ? Optional.empty()
|
||||
: Optional.of(new Task.ServiceProvider(serviceProvider.get().get("value").asText()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the first output data addition in this patch.
|
||||
*
|
||||
* @return the output data added in this patch or an empty {@link Optional} if none is found
|
||||
*/
|
||||
public Optional<Task.OutputData> extractFirstOutputDataAddition() {
|
||||
Optional<JsonNode> output = extractFirst(node ->
|
||||
isPatchAddOperation(node) && hasPath(node, "/outputData")
|
||||
);
|
||||
|
||||
return (output.isEmpty()) ? Optional.empty()
|
||||
: Optional.of(new Task.OutputData(output.get().get("value").asText()));
|
||||
}
|
||||
|
||||
private Optional<JsonNode> extractFirst(Predicate<? super JsonNode> predicate) {
|
||||
Stream<JsonNode> stream = StreamSupport.stream(patch.spliterator(), false);
|
||||
return stream.filter(predicate).findFirst();
|
||||
}
|
||||
|
||||
private boolean isPatchAddOperation(JsonNode node) {
|
||||
return isPatchOperationOfType(node, "add");
|
||||
}
|
||||
|
||||
private boolean isPatchReplaceOperation(JsonNode node) {
|
||||
return isPatchOperationOfType(node, "replace");
|
||||
}
|
||||
|
||||
private boolean isPatchOperationOfType(JsonNode node, String operation) {
|
||||
return node.isObject() && node.get("op") != null
|
||||
&& node.get("op").asText().equalsIgnoreCase(operation);
|
||||
}
|
||||
|
||||
private boolean hasPath(JsonNode node, String path) {
|
||||
return node.isObject() && node.get("path") != null
|
||||
&& node.get("path").asText().equalsIgnoreCase(path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
package ch.unisg.tapastasks.tasks.adapter.in.formats;
|
||||
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
/**
|
||||
* This class is used to expose and consume representations of tasks through the HTTP interface. The
|
||||
* representations conform to the custom JSON-based media type "application/task+json". The media type
|
||||
* is just an identifier and can be registered with
|
||||
* <a href="https://www.iana.org/assignments/media-types/">IANA</a> to promote interoperability.
|
||||
*/
|
||||
final public class TaskJsonRepresentation {
|
||||
// The media type used for this task representation format
|
||||
public static final String MEDIA_TYPE = "application/task+json";
|
||||
|
||||
// A task identifier specific to our implementation (e.g., a UUID). This identifier is then used
|
||||
// to generate the task's URI. URIs are standard uniform identifiers and use a universal syntax
|
||||
// that can be referenced (and dereferenced) independent of context. In our uniform HTTP API,
|
||||
// we identify tasks via URIs and not implementation-specific identifiers.
|
||||
@Getter @Setter
|
||||
private String taskId;
|
||||
|
||||
// A string that represents the task's name
|
||||
@Getter
|
||||
private final String taskName;
|
||||
|
||||
// A string that identifies the task's type. This string could also be a URI (e.g., defined in some
|
||||
// Web ontology, as we shall see later in the course), but it's not constrained to be a URI.
|
||||
// The task's type can be used to assign executors to tasks, to decide what tasks to bid for, etc.
|
||||
@Getter
|
||||
private final String taskType;
|
||||
|
||||
// The task's status: OPEN, ASSIGNED, RUNNING, or EXECUTED (see Task.Status)
|
||||
@Getter @Setter
|
||||
private String taskStatus;
|
||||
|
||||
// If this task is a delegated task (i.e., a shadow of another task), this URI points to the
|
||||
// original task. Because URIs are standard and uniform, we can just dereference this URI to
|
||||
// retrieve a representation of the original task.
|
||||
@Getter @Setter
|
||||
private String originalTaskUri;
|
||||
|
||||
// The service provider who executes this task. The service provider is a any string that identifies
|
||||
// a TAPAS group (e.g., tapas-group1). This identifier could also be a URI (if we have a good reason
|
||||
// for it), but it's not constrained to be a URI.
|
||||
@Getter @Setter
|
||||
private String serviceProvider;
|
||||
|
||||
// A string that provides domain-specific input data for this task. In the context of this project,
|
||||
// we can parse and interpret the input data based on the task's type.
|
||||
@Getter @Setter
|
||||
private String inputData;
|
||||
|
||||
// A string that provides domain-specific output data for this task. In the context of this project,
|
||||
// we can parse and interpret the output data based on the task's type.
|
||||
@Getter @Setter
|
||||
private String outputData;
|
||||
|
||||
/**
|
||||
* Instantiate a task representation with a task name and type.
|
||||
*
|
||||
* @param taskName string that represents the task's name
|
||||
* @param taskType string that represents the task's type
|
||||
*/
|
||||
public TaskJsonRepresentation(String taskName, String taskType) {
|
||||
this.taskName = taskName;
|
||||
this.taskType = taskType;
|
||||
|
||||
this.taskStatus = null;
|
||||
this.originalTaskUri = null;
|
||||
this.serviceProvider = null;
|
||||
this.inputData = null;
|
||||
this.outputData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a task representation from a domain concept.
|
||||
*
|
||||
* @param task the task
|
||||
*/
|
||||
public TaskJsonRepresentation(Task task) {
|
||||
this(task.getTaskName().getValue(), task.getTaskType().getValue());
|
||||
|
||||
this.taskId = task.getTaskId().getValue();
|
||||
this.taskStatus = task.getTaskStatus().getValue().name();
|
||||
|
||||
this.originalTaskUri = (task.getOriginalTaskUri() == null) ?
|
||||
null : task.getOriginalTaskUri().getValue();
|
||||
|
||||
this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue();
|
||||
this.inputData = (task.getInputData() == null) ? null : task.getInputData().getValue();
|
||||
this.outputData = (task.getOutputData() == null) ? null : task.getOutputData().getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method used to serialize a task provided as a domain concept in the format exposed
|
||||
* through the uniform HTTP API.
|
||||
*
|
||||
* @param task the task as defined in the domain
|
||||
* @return a string serialization using the JSON-based representation format defined for tasks
|
||||
* @throws JsonProcessingException if a runtime exception occurs during object serialization
|
||||
*/
|
||||
public static String serialize(Task task) throws JsonProcessingException {
|
||||
TaskJsonRepresentation representation = new TaskJsonRepresentation(task);
|
||||
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
|
||||
return mapper.writeValueAsString(representation);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package ch.unisg.tapastasks.tasks.adapter.in.messaging;
|
||||
|
||||
public class UnknownEventException extends RuntimeException { }
|
|
@ -0,0 +1,39 @@
|
|||
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
|
||||
|
||||
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
|
||||
import ch.unisg.tapastasks.tasks.application.handler.TaskAssignedHandler;
|
||||
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent;
|
||||
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task.TaskId;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Listener for task assigned events. A task assigned event corresponds to a JSON Patch that attempts
|
||||
* to change the task's status to ASSIGNED and may also add/replace a service provider (i.e., to what
|
||||
* group the task was assigned). This implementation does not impose that a task assigned event
|
||||
* includes the service provider (i.e., can be null).
|
||||
*
|
||||
* See also {@link TaskAssignedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
|
||||
*/
|
||||
public class TaskAssignedEventListenerHttpAdapter extends TaskEventListener {
|
||||
|
||||
/**
|
||||
* Handles the task assigned event.
|
||||
*
|
||||
* @param taskId the identifier of the task for which an event was received
|
||||
* @param payload the JSON Patch payload of the HTTP PATCH request received for this task
|
||||
* @return
|
||||
*/
|
||||
public Task handleTaskEvent(String taskId, JsonNode payload) {
|
||||
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
|
||||
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
|
||||
|
||||
TaskAssignedEvent taskAssignedEvent = new TaskAssignedEvent(new TaskId(taskId), serviceProvider);
|
||||
TaskAssignedEventHandler taskAssignedEventHandler = new TaskAssignedHandler();
|
||||
|
||||
return taskAssignedEventHandler.handleTaskAssigned(taskAssignedEvent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
|
||||
|
||||
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
|
||||
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
|
||||
import ch.unisg.tapastasks.tasks.adapter.in.messaging.UnknownEventException;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.github.fge.jsonpatch.JsonPatch;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
/**
|
||||
* This REST Controller handles HTTP PATCH requests for updating the representational state of Task
|
||||
* resources. Each request to update the representational state of a Task resource can correspond to
|
||||
* at most one domain/integration event. Request payloads use the
|
||||
* <a href="http://jsonpatch.com/">JSON PATCH</a> format and media type.
|
||||
*
|
||||
* A JSON Patch can contain multiple operations (e.g., add, remove, replace) for updating various
|
||||
* parts of a task's representations. One or more JSON Patch operations can represent a domain/integration
|
||||
* event. Therefore, the events can only be determined by inspecting the requested patch (e.g., a request
|
||||
* to change a task's status from RUNNING to EXECUTED). This class is responsible to inspect requested
|
||||
* patches, identify events, and to route them to appropriate listeners.
|
||||
*
|
||||
* For more details on JSON Patch, see: <a href="http://jsonpatch.com/">http://jsonpatch.com/</a>
|
||||
* For some sample HTTP requests, see the README.
|
||||
*/
|
||||
@RestController
|
||||
public class TaskEventHttpDispatcher {
|
||||
// The standard media type for JSON Patch registered with IANA
|
||||
// See: https://www.iana.org/assignments/media-types/application/json-patch+json
|
||||
private final static String JSON_PATCH_MEDIA_TYPE = "application/json-patch+json";
|
||||
|
||||
/**
|
||||
* Handles HTTP PATCH requests with a JSON Patch payload. Routes the requests based on the
|
||||
* the operations requested in the patch. In this implementation, one HTTP Patch request is
|
||||
* mapped to at most one domain event.
|
||||
*
|
||||
* @param taskId the local (i.e., implementation-specific) identifier of the task to the patched;
|
||||
* this identifier is extracted from the task's URI
|
||||
* @param payload the reuqested patch for this task
|
||||
* @return 200 OK and a representation of the task after processing the event; 404 Not Found if
|
||||
* the request URI does not match any task; 400 Bad Request if the request is invalid
|
||||
*/
|
||||
@PatchMapping(path = "/tasks/{taskId}", consumes = {JSON_PATCH_MEDIA_TYPE})
|
||||
public ResponseEntity<String> dispatchTaskEvents(@PathVariable("taskId") String taskId,
|
||||
@RequestBody JsonNode payload) {
|
||||
try {
|
||||
// Throw an exception if the JSON Patch format is invalid. This call is only used to
|
||||
// validate the JSON PATCH syntax.
|
||||
JsonPatch.fromJson(payload);
|
||||
|
||||
// Check for known events and route the events to appropriate listeners
|
||||
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
|
||||
Optional<Task.Status> status = representation.extractFirstTaskStatusChange();
|
||||
|
||||
TaskEventListener listener = null;
|
||||
|
||||
// Route events related to task status changes
|
||||
if (status.isPresent()) {
|
||||
switch (status.get()) {
|
||||
case ASSIGNED:
|
||||
listener = new TaskAssignedEventListenerHttpAdapter();
|
||||
break;
|
||||
case RUNNING:
|
||||
listener = new TaskStartedEventListenerHttpAdapter();
|
||||
break;
|
||||
case EXECUTED:
|
||||
listener = new TaskExecutedEventListenerHttpAdapter();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
// The HTTP PATCH request is valid, but the patch does not match any known event
|
||||
throw new UnknownEventException();
|
||||
}
|
||||
|
||||
Task task = listener.handleTaskEvent(taskId, payload);
|
||||
|
||||
// Add the content type as a response header
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
|
||||
|
||||
return new ResponseEntity<>(TaskJsonRepresentation.serialize(task), responseHeaders,
|
||||
HttpStatus.OK);
|
||||
} catch (TaskNotFoundException e) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
|
||||
|
||||
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
|
||||
import ch.unisg.tapastasks.tasks.application.handler.TaskExecutedHandler;
|
||||
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent;
|
||||
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Listener for task executed events. A task executed event corresponds to a JSON Patch that attempts
|
||||
* to change the task's status to EXECUTED, may add/replace a service provider, and may also add an
|
||||
* output result. This implementation does not impose that a task executed event includes either the
|
||||
* service provider or an output result (i.e., both can be null).
|
||||
*
|
||||
* See also {@link TaskExecutedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
|
||||
*/
|
||||
public class TaskExecutedEventListenerHttpAdapter extends TaskEventListener {
|
||||
|
||||
public Task handleTaskEvent(String taskId, JsonNode payload) {
|
||||
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
|
||||
|
||||
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
|
||||
Optional<Task.OutputData> outputData = representation.extractFirstOutputDataAddition();
|
||||
|
||||
TaskExecutedEvent taskExecutedEvent = new TaskExecutedEvent(new Task.TaskId(taskId),
|
||||
serviceProvider, outputData);
|
||||
TaskExecutedEventHandler taskExecutedEventHandler = new TaskExecutedHandler();
|
||||
|
||||
return taskExecutedEventHandler.handleTaskExecuted(taskExecutedEvent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
|
||||
|
||||
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
|
||||
import ch.unisg.tapastasks.tasks.application.handler.TaskStartedHandler;
|
||||
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent;
|
||||
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task.TaskId;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Listener for task started events. A task started event corresponds to a JSON Patch that attempts
|
||||
* to change the task's status to RUNNING and may also add/replace a service provider. This
|
||||
* implementation does not impose that a task started event includes the service provider (i.e.,
|
||||
* can be null).
|
||||
*
|
||||
* See also {@link TaskStartedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
|
||||
*/
|
||||
public class TaskStartedEventListenerHttpAdapter extends TaskEventListener {
|
||||
|
||||
public Task handleTaskEvent(String taskId, JsonNode payload) {
|
||||
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
|
||||
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
|
||||
|
||||
TaskStartedEvent taskStartedEvent = new TaskStartedEvent(new TaskId(taskId), serviceProvider);
|
||||
TaskStartedEventHandler taskStartedEventHandler = new TaskStartedHandler();
|
||||
|
||||
return taskStartedEventHandler.handleTaskStarted(taskStartedEvent);
|
||||
}
|
||||
}
|
|
@ -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<String> addNewTaskTaskToTaskList(@RequestBody Task task) {
|
||||
@PostMapping(path = "/tasks/", consumes = {TaskJsonRepresentation.MEDIA_TYPE})
|
||||
public ResponseEntity<String> 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<Task.OriginalTaskUri> originalTaskUriOptional =
|
||||
(payload.getOriginalTaskUri() == null) ? Optional.empty()
|
||||
: Optional.of(new Task.OriginalTaskUri(payload.getOriginalTaskUri()));
|
||||
|
||||
AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(taskName, taskType,
|
||||
originalTaskUriOptional);
|
||||
|
||||
Task createdTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command);
|
||||
|
||||
// When creating a task, the task's representation may include optional input data
|
||||
if (payload.getInputData() != null) {
|
||||
createdTask.setInputData(new Task.InputData(payload.getInputData()));
|
||||
}
|
||||
|
||||
// Add the content type as a response header
|
||||
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());
|
||||
}
|
||||
|
|
|
@ -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<String> retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) {
|
||||
RetrieveTaskFromTaskListQuery command = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId));
|
||||
Optional<Task> updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(command);
|
||||
RetrieveTaskFromTaskListQuery query = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId));
|
||||
Optional<Task> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() { }
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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<AddNewTaskToTaskListCommand> {
|
||||
@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<Task.OriginalTaskUri> originalTaskUri;
|
||||
|
||||
public AddNewTaskToTaskListCommand(Task.TaskName taskName, Task.TaskType taskType,
|
||||
Optional<Task.OriginalTaskUri> originalTaskUri) {
|
||||
this.taskName = taskName;
|
||||
this.taskType = taskType;
|
||||
this.originalTaskUri = originalTaskUri;
|
||||
|
||||
this.validateSelf();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,5 +5,5 @@ import ch.unisg.tapastasks.tasks.domain.Task;
|
|||
import java.util.Optional;
|
||||
|
||||
public interface RetrieveTaskFromTaskListUseCase {
|
||||
Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command);
|
||||
Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package ch.unisg.tapastasks.tasks.application.port.in;
|
||||
|
||||
import ch.unisg.tapastasks.common.SelfValidating;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Optional;
|
||||
|
||||
@Value
|
||||
public class TaskAssignedEvent extends SelfValidating<TaskAssignedEvent> {
|
||||
@NotNull
|
||||
private final Task.TaskId taskId;
|
||||
|
||||
@Getter
|
||||
private final Optional<Task.ServiceProvider> serviceProvider;
|
||||
|
||||
public TaskAssignedEvent(Task.TaskId taskId, Optional<Task.ServiceProvider> serviceProvider) {
|
||||
this.taskId = taskId;
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
this.validateSelf();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package ch.unisg.tapastasks.tasks.application.port.in;
|
||||
|
||||
import ch.unisg.tapastasks.common.SelfValidating;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Optional;
|
||||
|
||||
@Value
|
||||
public class TaskExecutedEvent extends SelfValidating<TaskExecutedEvent> {
|
||||
@NotNull
|
||||
private final TaskId taskId;
|
||||
|
||||
@Getter
|
||||
private final Optional<ServiceProvider> serviceProvider;
|
||||
|
||||
@Getter
|
||||
private final Optional<OutputData> outputData;
|
||||
|
||||
public TaskExecutedEvent(TaskId taskId, Optional<ServiceProvider> serviceProvider,
|
||||
Optional<OutputData> outputData) {
|
||||
this.taskId = taskId;
|
||||
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.outputData = outputData;
|
||||
|
||||
this.validateSelf();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package ch.unisg.tapastasks.tasks.application.port.in;
|
||||
|
||||
import ch.unisg.tapastasks.common.SelfValidating;
|
||||
import ch.unisg.tapastasks.tasks.domain.Task;
|
||||
import lombok.Getter;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.Optional;
|
||||
|
||||
@Value
|
||||
public class TaskStartedEvent extends SelfValidating<TaskStartedEvent> {
|
||||
@NotNull
|
||||
private final Task.TaskId taskId;
|
||||
|
||||
@Getter
|
||||
private final Optional<Task.ServiceProvider> serviceProvider;
|
||||
|
||||
public TaskStartedEvent(Task.TaskId taskId, Optional<Task.ServiceProvider> serviceProvider) {
|
||||
this.taskId = taskId;
|
||||
this.serviceProvider = serviceProvider;
|
||||
|
||||
this.validateSelf();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -15,8 +15,8 @@ import java.util.Optional;
|
|||
@Transactional
|
||||
public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase {
|
||||
@Override
|
||||
public Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command) {
|
||||
public Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query) {
|
||||
TaskList taskList = TaskList.getTapasTaskList();
|
||||
return taskList.retrieveTaskById(command.getTaskId());
|
||||
return taskList.retrieveTaskById(query.getTaskId());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Task> retrieveTaskById(Task.TaskId id) {
|
||||
|
@ -55,6 +67,43 @@ public class TaskList {
|
|||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Task changeTaskStatusToAssigned(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider)
|
||||
throws TaskNotFoundException {
|
||||
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty());
|
||||
}
|
||||
|
||||
public Task changeTaskStatusToRunning(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider)
|
||||
throws TaskNotFoundException {
|
||||
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty());
|
||||
}
|
||||
|
||||
public Task changeTaskStatusToExecuted(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider,
|
||||
Optional<Task.OutputData> outputData) throws TaskNotFoundException {
|
||||
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData);
|
||||
}
|
||||
|
||||
private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional<Task.ServiceProvider> serviceProvider,
|
||||
Optional<Task.OutputData> outputData) {
|
||||
Optional<Task> taskOpt = retrieveTaskById(id);
|
||||
|
||||
if (taskOpt.isEmpty()) {
|
||||
throw new TaskNotFoundException();
|
||||
}
|
||||
|
||||
Task task = taskOpt.get();
|
||||
task.setTaskStatus(status);
|
||||
|
||||
if (serviceProvider.isPresent()) {
|
||||
task.setProvider(serviceProvider.get());
|
||||
}
|
||||
|
||||
if (outputData.isPresent()) {
|
||||
task.setOutputData(outputData.get());
|
||||
}
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
@Value
|
||||
public static class TaskListName {
|
||||
private String value;
|
||||
|
@ -64,5 +113,4 @@ public class TaskList {
|
|||
public static class ListOfTasks {
|
||||
private List<Task> value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
package ch.unisg.tapastasks.tasks.domain;
|
||||
|
||||
public class TaskNotFoundException extends RuntimeException { }
|
|
@ -1 +1,2 @@
|
|||
server.port=8081
|
||||
baseuri=https://tapas-tasks.86-119-34-23.nip.io/
|
||||
|
|
Loading…
Reference in New Issue
Block a user