From 34fb3c682fb9a6527a2d252f8fd23979a49b14c4 Mon Sep 17 00:00:00 2001 From: Andrei Ciortea Date: Mon, 18 Oct 2021 01:19:42 +0200 Subject: [PATCH 01/40] Add Auction House; Extend uniform HTTP API for TAPAS-Tasks --- .github/workflows/build-and-deploy.yml | 6 +- docker-compose.yml | 15 + tapas-auction-house/.editorconfig | 9 + tapas-auction-house/.gitignore | 33 ++ .../.mvn/wrapper/MavenWrapperDownloader.java | 117 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + tapas-auction-house/README.md | 101 ++++++ tapas-auction-house/mvnw | 310 ++++++++++++++++++ tapas-auction-house/mvnw.cmd | 182 ++++++++++ tapas-auction-house/pom.xml | 80 +++++ .../tapas/TapasAuctionHouseApplication.java | 71 ++++ .../common/clients/TapasMqttClient.java | 94 ++++++ .../common/clients/WebSubSubscriber.java | 28 ++ .../formats/AuctionJsonRepresentation.java | 60 ++++ ...ExecutorAddedEventListenerHttpAdapter.java | 35 ++ ...ecutorRemovedEventListenerHttpAdapter.java | 16 + .../mqtt/AuctionEventMqttListener.java | 11 + .../mqtt/AuctionEventsMqttDispatcher.java | 51 +++ ...ExecutorAddedEventListenerMqttAdapter.java | 46 +++ ...tionStartedEventListenerWebSubAdapter.java | 18 + .../in/web/LaunchAuctionWebController.java | 72 ++++ .../RetrieveOpenAuctionsWebController.java | 62 ++++ ...blishAuctionStartedEventWebSubAdapter.java | 37 +++ .../out/web/AuctionWonEventHttpAdapter.java | 20 ++ .../PlaceBidForAuctionCommandHttpAdapter.java | 19 ++ .../handler/AuctionStartedHandler.java | 59 ++++ .../handler/ExecutorAddedHandler.java | 16 + .../handler/ExecutorRemovedHandler.java | 19 ++ .../port/in/AuctionStartedEvent.java | 21 ++ .../port/in/AuctionStartedEventHandler.java | 6 + .../port/in/ExecutorAddedEvent.java | 32 ++ .../port/in/ExecutorAddedEventHandler.java | 6 + .../port/in/ExecutorRemovedEvent.java | 26 ++ .../port/in/ExecutorRemovedEventHandler.java | 6 + .../port/in/LaunchAuctionCommand.java | 37 +++ .../port/in/LaunchAuctionUseCase.java | 8 + .../port/in/RetrieveOpenAuctionsQuery.java | 7 + .../port/in/RetrieveOpenAuctionsUseCase.java | 10 + .../port/out/AuctionStartedEventPort.java | 11 + .../port/out/AuctionWonEventPort.java | 11 + .../port/out/PlaceBidForAuctionCommand.java | 25 ++ .../out/PlaceBidForAuctionCommandPort.java | 6 + .../service/RetrieveOpenAuctionsService.java | 22 ++ .../service/StartAuctionService.java | 113 +++++++ .../tapas/auctionhouse/domain/Auction.java | 171 ++++++++++ .../auctionhouse/domain/AuctionRegistry.java | 105 ++++++ .../domain/AuctionStartedEvent.java | 15 + .../auctionhouse/domain/AuctionWonEvent.java | 16 + .../unisg/tapas/auctionhouse/domain/Bid.java | 66 ++++ .../auctionhouse/domain/ExecutorRegistry.java | 86 +++++ .../common/AuctionHouseResourceDirectory.java | 57 ++++ .../unisg/tapas/common/ConfigProperties.java | 64 ++++ .../ch/unisg/tapas/common/SelfValidating.java | 25 ++ .../src/main/resources/application.properties | 8 + .../TapasAuctionHouseApplicationTests.java | 13 + tapas-tasks/README.md | 152 +++++++-- tapas-tasks/pom.xml | 17 +- .../tapastasks/TapasTasksApplication.java | 3 - .../formats/TaskJsonPatchRepresentation.java | 102 ++++++ .../in/formats/TaskJsonRepresentation.java | 115 +++++++ .../in/messaging/UnknownEventException.java | 3 + .../TaskAssignedEventListenerHttpAdapter.java | 39 +++ .../http/TaskEventHttpDispatcher.java | 103 ++++++ .../in/messaging/http/TaskEventListener.java | 24 ++ .../TaskExecutedEventListenerHttpAdapter.java | 34 ++ .../TaskStartedEventListenerHttpAdapter.java | 32 ++ .../AddNewTaskToTaskListWebController.java | 58 +++- ...RetrieveTaskFromTaskListWebController.java | 33 +- .../tasks/adapter/in/web/TaskMediaType.java | 23 -- .../handler/TaskAssignedHandler.java | 19 ++ .../handler/TaskExecutedHandler.java | 19 ++ .../handler/TaskStartedHandler.java | 19 ++ .../port/in/AddNewTaskToTaskListCommand.java | 17 +- .../in/RetrieveTaskFromTaskListUseCase.java | 2 +- .../port/in/TaskAssignedEvent.java | 25 ++ .../port/in/TaskAssignedEventHandler.java | 8 + .../port/in/TaskExecutedEvent.java | 34 ++ .../port/in/TaskExecutedEventHandler.java | 8 + .../application/port/in/TaskStartedEvent.java | 28 ++ .../port/in/TaskStartedEventHandler.java | 8 + .../service/AddNewTaskToTaskListService.java | 8 +- .../RetrieveTaskFromTaskListService.java | 4 +- .../unisg/tapastasks/tasks/domain/Task.java | 71 +++- .../tapastasks/tasks/domain/TaskList.java | 64 +++- .../tasks/domain/TaskNotFoundException.java | 3 + .../src/main/resources/application.properties | 1 + 87 files changed, 3532 insertions(+), 106 deletions(-) create mode 100644 tapas-auction-house/.editorconfig create mode 100644 tapas-auction-house/.gitignore create mode 100644 tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 tapas-auction-house/.mvn/wrapper/maven-wrapper.jar create mode 100644 tapas-auction-house/.mvn/wrapper/maven-wrapper.properties create mode 100644 tapas-auction-house/README.md create mode 100755 tapas-auction-house/mvnw create mode 100644 tapas-auction-house/mvnw.cmd create mode 100644 tapas-auction-house/pom.xml create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java create mode 100644 tapas-auction-house/src/main/resources/application.properties create mode 100644 tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java delete mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 619804e..2c5b192 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -27,7 +27,11 @@ jobs: - name: Build with Maven run: mvn -f tapas-tasks/pom.xml --batch-mode --update-snapshots verify - - run: cp ./tapas-tasks/target/tapas-tasks-0.0.1-SNAPSHOT.jar ./target + - run: cp ./tapas-tasks/target/tapas-tasks-0.0.1-SNAPSHOT.jar ./target + + - name: Build with Maven + run: mvn -f tapas-auction-house/pom.xml --batch-mode --update-snapshots verify + - run: cp ./tapas-auction-house/target/tapas-auction-house-0.0.1-SNAPSHOT.jar ./target - name: Build with Maven run: mvn -f app/pom.xml --batch-mode --update-snapshots verify diff --git a/docker-compose.yml b/docker-compose.yml index 6d8f256..dad6b78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,6 +41,21 @@ services: - "traefik.http.routers.tapas-tasks.entryPoints=web,websecure" - "traefik.http.routers.tapas-tasks.tls.certresolver=le" + auction-house: + image: openjdk + command: "java -jar /data/tapas-auction-house-0.0.1-SNAPSHOT.jar" + restart: unless-stopped + volumes: + - ./:/data/ + labels: + - "traefik.enable=true" + - "traefik.http.routers.tapas-auction-house.rule=Host(`tapas-auction-house.${PUB_IP}.nip.io`)" + - "traefik.http.routers.tapas-auction-house.service=tapas-tasks" + - "traefik.http.services.tapas-auction-house.loadbalancer.server.port=8082" + - "traefik.http.routers.tapas-auction-house.tls=true" + - "traefik.http.routers.tapas-auction-house.entryPoints=web,websecure" + - "traefik.http.routers.tapas-auction-house.tls.certresolver=le" + app: image: openjdk command: "java -jar /data/app-0.1.0.jar" diff --git a/tapas-auction-house/.editorconfig b/tapas-auction-house/.editorconfig new file mode 100644 index 0000000..c4f3e5b --- /dev/null +++ b/tapas-auction-house/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true diff --git a/tapas-auction-house/.gitignore b/tapas-auction-house/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/tapas-auction-house/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java b/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..e76d1f3 --- /dev/null +++ b/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/tapas-auction-house/.mvn/wrapper/maven-wrapper.jar b/tapas-auction-house/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties b/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..ffdc10e --- /dev/null +++ b/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/tapas-auction-house/README.md b/tapas-auction-house/README.md new file mode 100644 index 0000000..683500d --- /dev/null +++ b/tapas-auction-house/README.md @@ -0,0 +1,101 @@ +# tapas-auction-house + +The Auction House is the part of your TAPAS application that is largely responsible for the interactions +with the TAPAS applications developed by the other groups. More precisely, it is responsible for +launching and managing auctions and it is implemented following the Hexagonal Architecture (based on +examples from book "Get Your Hands Dirty on Clean Architecture" by Tom Hombergs). + +Technologies: Spring Boot, Maven + +**Note:** this repository contains an [EditorConfig](https://editorconfig.org/) file (`.editorconfig`) +with default editor settings. EditorConfig is supported out-of-the-box by the IntelliJ IDE. To help maintain +consistent code styles, we recommend to reuse this editor configuration file in all your services. + +## Project Overview + +This project provides a partial implementation of the Auction House. The code is documented in detail, +here we only include a summary of implemented features: +* running and managing auctions: + * each auction has a deadline by which it is open for bids + * once the deadline has passed, the auction house closes the auction and selects a random bid +* starting an auction using a command via an HTTP adapter (see sample request below) +* retrieving the list of open auctions via an HTTP adapter, i.e. auctions accepting bids (see sample + request below) +* receiving events when executors are added to the TAPAS application (both via HTTP and MQTT adapters) +* the logic for automatic placement of bids in auctions: the auction house will place a bid in every + auction for which there is at least one executor that can handle the type of task + being auctioned +* discovery of auction houses via a provided resource directory (see assignment sheet for + Exercises 5 & 6 for more details) + +## Overview of Adapters + +In addition to the overall skeleton of the auction house, the current partial implementation provides +several adapters to help you get started. + +### HTTP Adapters + +Sample HTTP request for launching an auction: + +```shell +curl -i --location --request POST 'http://localhost:8083/auctions/' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "taskUri" : "http://example.org", + "taskType" : "taskType1", + "deadline" : 10000 +}' + +HTTP/1.1 201 +Content-Type: application/json +Content-Length: 131 +Date: Sun, 17 Oct 2021 22:34:13 GMT + +{ + "auctionId":"1", + "auctionHouseUri":"http://localhost:8083/", + "taskUri":"http://example.org", + "taskType":"taskType1", + "deadline":10000 +} +``` + +Sample HTTP request for retrieving auctions currently open for bids: + +```shell +curl -i --location --request GET 'http://localhost:8083/auctions/' + +HTTP/1.1 200 +Content-Type: application/json +Content-Length: 133 +Date: Sun, 17 Oct 2021 22:34:20 GMT + +[ + { + "auctionId":"1", + "auctionHouseUri":"http://localhost:8083/", + "taskUri":"http://example.org", + "taskType":"taskType1", + "deadline":10000 + } +] +``` + +Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java) +via an HTTP request: + +```shell +curl -i --location --request POST 'http://localhost:8083/executors/taskType1/executor1' + +HTTP/1.1 204 +Date: Sun, 17 Oct 2021 22:38:45 GMT +``` + +### MQTT Adapters + +Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java) +via an MQTT message via HiveMQ's [MQTT CLI](https://hivemq.github.io/mqtt-cli/): + +```shell + mqtt pub -t ch/unisg/tapas-group1/executors -m '{ "taskType" : "taskType1", "executorId" : "executor1" }' +``` diff --git a/tapas-auction-house/mvnw b/tapas-auction-house/mvnw new file mode 100755 index 0000000..a16b543 --- /dev/null +++ b/tapas-auction-house/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/tapas-auction-house/mvnw.cmd b/tapas-auction-house/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/tapas-auction-house/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/tapas-auction-house/pom.xml b/tapas-auction-house/pom.xml new file mode 100644 index 0000000..4b9cbb6 --- /dev/null +++ b/tapas-auction-house/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.3 + + + ch.unisg + tapas-auction-house + 0.0.1-SNAPSHOT + tapas-auction-house + TAPAS Auction House + + 11 + + + + Eclipse Paho Repo + https://repo.eclipse.org/content/repositories/paho-releases/ + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.0 + + + javax.transaction + javax.transaction-api + 1.2 + + + javax.validation + validation-api + 1.1.0.Final + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java new file mode 100644 index 0000000..8fc22d0 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -0,0 +1,71 @@ +package ch.unisg.tapas; + +import ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient; +import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher; +import ch.unisg.tapas.auctionhouse.adapter.common.clients.WebSubSubscriber; +import ch.unisg.tapas.common.AuctionHouseResourceDirectory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.net.URI; +import java.util.List; + +/** + * Main TAPAS Auction House application. + */ +@SpringBootApplication +public class TapasAuctionHouseApplication { + private static final Logger LOGGER = LogManager.getLogger(TapasAuctionHouseApplication.class); + + public static String RESOURCE_DIRECTORY = "https://api.interactions.ics.unisg.ch/auction-houses/"; + public static String MQTT_BROKER = "tcp://broker.hivemq.com:1883"; + + public static void main(String[] args) { + SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class); + + // We will use these bootstrap methods in Week 6: + // bootstrapMarketplaceWithWebSub(); + // bootstrapMarketplaceWithMqtt(); + + tapasAuctioneerApp.run(args); + } + + /** + * Discovers auction houses and subscribes to WebSub notifications + */ + private static void bootstrapMarketplaceWithWebSub() { + List auctionHouseEndpoints = discoverAuctionHouseEndpoints(); + LOGGER.info("Found auction house endpoints: " + auctionHouseEndpoints); + + WebSubSubscriber subscriber = new WebSubSubscriber(); + + for (String endpoint : auctionHouseEndpoints) { + subscriber.subscribeToAuctionHouseEndpoint(URI.create(endpoint)); + } + } + + /** + * Connects to an MQTT broker, presumably the one used by all TAPAS groups to communicate with + * one another + */ + private static void bootstrapMarketplaceWithMqtt() { + try { + AuctionEventsMqttDispatcher dispatcher = new AuctionEventsMqttDispatcher(); + TapasMqttClient client = TapasMqttClient.getInstance(MQTT_BROKER, dispatcher); + client.startReceivingMessages(); + } catch (MqttException e) { + LOGGER.error(e.getMessage(), e); + } + } + + private static List discoverAuctionHouseEndpoints() { + AuctionHouseResourceDirectory rd = new AuctionHouseResourceDirectory( + URI.create(RESOURCE_DIRECTORY) + ); + + return rd.retrieveAuctionHouseEndpoints(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java new file mode 100644 index 0000000..708d512 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java @@ -0,0 +1,94 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.clients; + +import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * MQTT client for your TAPAS application. This class is defined as a singleton, but it does not have + * to be this way. This class is only provided as an example to help you bootstrap your project. + * You are welcomed to change this class as you see fit. + */ +public class TapasMqttClient { + private static final Logger LOGGER = LogManager.getLogger(TapasMqttClient.class); + + private static TapasMqttClient tapasClient = null; + + private MqttClient mqttClient; + private final String mqttClientId; + private final String brokerAddress; + + private final MessageReceivedCallback messageReceivedCallback; + + private final AuctionEventsMqttDispatcher dispatcher; + + private TapasMqttClient(String brokerAddress, AuctionEventsMqttDispatcher dispatcher) { + this.mqttClientId = UUID.randomUUID().toString(); + this.brokerAddress = brokerAddress; + + this.messageReceivedCallback = new MessageReceivedCallback(); + + this.dispatcher = dispatcher; + } + + public static synchronized TapasMqttClient getInstance(String brokerAddress, + AuctionEventsMqttDispatcher dispatcher) { + + if (tapasClient == null) { + tapasClient = new TapasMqttClient(brokerAddress, dispatcher); + } + + return tapasClient; + } + + public void startReceivingMessages() throws MqttException { + mqttClient = new org.eclipse.paho.client.mqttv3.MqttClient(brokerAddress, mqttClientId, new MemoryPersistence()); + mqttClient.connect(); + mqttClient.setCallback(messageReceivedCallback); + + subscribeToAllTopics(); + } + + public void stopReceivingMessages() throws MqttException { + mqttClient.disconnect(); + } + + private void subscribeToAllTopics() throws MqttException { + for (String topic : dispatcher.getAllTopics()) { + subscribeToTopic(topic); + } + } + + private void subscribeToTopic(String topic) throws MqttException { + mqttClient.subscribe(topic); + } + + private void publishMessage(String topic, String payload) throws MqttException { + MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); + mqttClient.publish(topic, message); + } + + private class MessageReceivedCallback implements MqttCallback { + + @Override + public void connectionLost(Throwable cause) { } + + @Override + public void messageArrived(String topic, MqttMessage message) { + LOGGER.info("Received new MQTT message for topic " + topic + ": " + + new String(message.getPayload())); + + if (topic != null && !topic.isEmpty()) { + dispatcher.dispatchEvent(topic, message); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java new file mode 100644 index 0000000..da2b096 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java @@ -0,0 +1,28 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.clients; + +import java.net.URI; + +/** + * Subscribes to the WebSub hubs of auction houses discovered at run time. This class is instantiated + * from {@link ch.unisg.tapas.TapasAuctionHouseApplication} when boostraping the TAPAS marketplace + * via WebSub. + */ +public class WebSubSubscriber { + + public void subscribeToAuctionHouseEndpoint(URI endpoint) { + // TODO Subscribe to the auction house endpoint via WebSub: + // 1. Send a request to the auction house in order to discover the WebSub hub to subscribe to. + // The request URI should depend on the design of the Auction House HTTP API. + // 2. Send a subscription request to the discovered WebSub hub to subscribe to events relevant + // for this auction house. + // 3. Handle the validation of intent from the WebSub hub (see WebSub protocol). + // + // Once the subscription is activated, the hub will send "fat pings" with content updates. + // The content received from the hub will depend primarily on the design of the Auction House + // HTTP API. + // + // For further details see: + // - W3C WebSub Recommendation: https://www.w3.org/TR/websub/ + // - the implementation notes of the WebSub hub you are using to distribute events + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java new file mode 100644 index 0000000..4500423 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java @@ -0,0 +1,60 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.formats; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; + +/** + * Used to expose a representation of the state of an auction through an interface. This class is + * only meant as a starting point when defining a uniform HTTP API for the Auction House: feel free + * to modify this class as you see fit! + */ +public class AuctionJsonRepresentation { + public static final String MEDIA_TYPE = "application/json"; + + @Getter @Setter + private String auctionId; + + @Getter @Setter + private String auctionHouseUri; + + @Getter @Setter + private String taskUri; + + @Getter @Setter + private String taskType; + + @Getter @Setter + private Integer deadline; + + public AuctionJsonRepresentation() { } + + public AuctionJsonRepresentation(String auctionId, String auctionHouseUri, String taskUri, + String taskType, Integer deadline) { + this.auctionId = auctionId; + this.auctionHouseUri = auctionHouseUri; + this.taskUri = taskUri; + this.taskType = taskType; + this.deadline = deadline; + } + + public AuctionJsonRepresentation(Auction auction) { + this.auctionId = auction.getAuctionId().getValue(); + this.auctionHouseUri = auction.getAuctionHouseUri().getValue().toString(); + this.taskUri = auction.getTaskUri().getValue().toString(); + this.taskType = auction.getTaskType().getValue(); + this.deadline = auction.getDeadline().getValue(); + } + + public static String serialize(Auction auction) throws JsonProcessingException { + AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return mapper.writeValueAsString(representation); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java new file mode 100644 index 0000000..999c61c --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java @@ -0,0 +1,35 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http; + +import ch.unisg.tapas.auctionhouse.application.handler.ExecutorAddedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +/* + THIS CLASS WILL BE PROVIDED ONLY AS A TEMPLATE; POINT OUT THE API NEEDS TO BE DEFINED +*/ + +@RestController +public class ExecutorAddedEventListenerHttpAdapter { + + @PostMapping(path = "/executors/{taskType}/{executorId}") + public ResponseEntity handleExecutorAddedEvent(@PathVariable("taskType") String taskType, + @PathVariable("executorId") String executorId) { + + ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( + new ExecutorRegistry.ExecutorIdentifier(executorId), + new Auction.AuctionedTaskType(taskType) + ); + + ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler(); + newExecutorHandler.handleNewExecutorEvent(executorAddedEvent); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java new file mode 100644 index 0000000..53811f9 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java @@ -0,0 +1,16 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http; + +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * Template for handling an executor removed event received via an HTTP request + */ +@RestController +public class ExecutorRemovedEventListenerHttpAdapter { + + // TODO: add annotations for request method, request URI, etc. + public void handleExecutorRemovedEvent(@PathVariable("executorId") String executorId) { + // TODO: implement logic + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java new file mode 100644 index 0000000..6da39e6 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java @@ -0,0 +1,11 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * Abstract MQTT listener for auction-related events + */ +public abstract class AuctionEventMqttListener { + + public abstract boolean handleEvent(MqttMessage message); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java new file mode 100644 index 0000000..e5eaf12 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java @@ -0,0 +1,51 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import org.eclipse.paho.client.mqttv3.*; + +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +/** + * Dispatches MQTT messages for known topics to associated event listeners. Used in conjunction with + * {@link ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient}. + * + * This is where you would define MQTT topics and map them to event listeners (see + * {@link AuctionEventsMqttDispatcher#initRouter()}). + * + * This class is only provided as an example to help you bootstrap the project. You are welcomed to + * change this class as you see fit. + */ +public class AuctionEventsMqttDispatcher { + private final Map router; + + public AuctionEventsMqttDispatcher() { + this.router = new Hashtable<>(); + initRouter(); + } + + // TODO: Register here your topics and event listener adapters + private void initRouter() { + router.put("ch/unisg/tapas-group-tutors/executors", new ExecutorAddedEventListenerMqttAdapter()); + } + + /** + * Returns all topics registered with this dispatcher. + * + * @return the set of registered topics + */ + public Set getAllTopics() { + return router.keySet(); + } + + /** + * Dispatches an event received via MQTT for a given topic. + * + * @param topic the topic for which the MQTT message was received + * @param message the received MQTT message + */ + public void dispatchEvent(String topic, MqttMessage message) { + AuctionEventMqttListener listener = router.get(topic); + listener.handleEvent(message); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java new file mode 100644 index 0000000..87413f0 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java @@ -0,0 +1,46 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import ch.unisg.tapas.auctionhouse.application.handler.ExecutorAddedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * Listener that handles events when an executor was added to this TAPAS application. + * + * This class is only provided as an example to help you bootstrap the project. + */ +public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListener { + + @Override + public boolean handleEvent(MqttMessage message) { + String payload = new String(message.getPayload()); + + try { + // Note: this messge representation is provided only as an example. You should use a + // representation that makes sense in the context of your application. + JsonNode data = new ObjectMapper().readTree(payload); + + String taskType = data.get("taskType").asText(); + String executorId = data.get("executorId").asText(); + + ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( + new ExecutorRegistry.ExecutorIdentifier(executorId), + new Auction.AuctionedTaskType(taskType) + ); + + ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler(); + newExecutorHandler.handleNewExecutorEvent(executorAddedEvent); + } catch (JsonProcessingException | NullPointerException e) { + // TODO: refactor logging + e.printStackTrace(); + return false; + } + + return true; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java new file mode 100644 index 0000000..d156452 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java @@ -0,0 +1,18 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub; + +import ch.unisg.tapas.auctionhouse.application.handler.AuctionStartedHandler; +import org.springframework.web.bind.annotation.*; + +/** + * This class is a template for handling auction started events received via WebSub + */ +@RestController +public class AuctionStartedEventListenerWebSubAdapter { + private final AuctionStartedHandler auctionStartedHandler; + + public AuctionStartedEventListenerWebSubAdapter(AuctionStartedHandler auctionStartedHandler) { + this.auctionStartedHandler = auctionStartedHandler; + } + + //TODO +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java new file mode 100644 index 0000000..c65631e --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java @@ -0,0 +1,72 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.web; + +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.net.URI; + +/** + * Controller that handles HTTP requests for launching auctions. This controller implements the + * {@link LaunchAuctionUseCase} use case using the {@link LaunchAuctionCommand}. + */ +@RestController +public class LaunchAuctionWebController { + private final LaunchAuctionUseCase launchAuctionUseCase; + + /** + * Constructs the controller. + * + * @param launchAuctionUseCase an implementation of the launch auction use case + */ + public LaunchAuctionWebController(LaunchAuctionUseCase launchAuctionUseCase) { + this.launchAuctionUseCase = launchAuctionUseCase; + } + + /** + * Handles HTTP POST requests for launching auctions. Note: you are free to modify this handler + * as you see fit to reflect the discussions for the uniform HTTP API for the auction house. + * You should also ensure that this handler has the exact behavior you would expect from the + * defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.) + * + * @param payload a representation of the auction to be launched + * @return + */ + @PostMapping(path = "/auctions/", consumes = AuctionJsonRepresentation.MEDIA_TYPE) + public ResponseEntity launchAuction(@RequestBody AuctionJsonRepresentation payload) { + Auction.AuctionDeadline deadline = (payload.getDeadline() == null) ? + null : new Auction.AuctionDeadline(payload.getDeadline()); + + LaunchAuctionCommand command = new LaunchAuctionCommand( + new Auction.AuctionedTaskUri(URI.create(payload.getTaskUri())), + new Auction.AuctionedTaskType(payload.getTaskType()), + deadline + ); + + // This command returns the created auction. We need the created auction to be able to + // include a representation of it in the HTTP response. + Auction auction = launchAuctionUseCase.launchAuction(command); + + try { + AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction); + String auctionJson = AuctionJsonRepresentation.serialize(auction); + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, AuctionJsonRepresentation.MEDIA_TYPE); + + // Return a 201 Created status code and a representation of the created auction + return new ResponseEntity<>(auctionJson, responseHeaders, HttpStatus.CREATED); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java new file mode 100644 index 0000000..bcbf38c --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java @@ -0,0 +1,62 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.web; + +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery; +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; + +/** + * Controller that handles HTTP requests for retrieving auctions hosted by this auction house that + * are open for bids. This controller implements the {@link RetrieveOpenAuctionsUseCase} use case + * using the {@link RetrieveOpenAuctionsQuery}. + */ +@RestController +public class RetrieveOpenAuctionsWebController { + private final RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase; + + public RetrieveOpenAuctionsWebController(RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase) { + this.retrieveAuctionListUseCase = retrieveAuctionListUseCase; + } + + /** + * Handles HTTP GET requests to retrieve the auctions that are open. Note: you are free to modify + * this handler as you see fit to reflect the discussions for the uniform HTTP API for the + * auction house. You should also ensure that this handler has the exact behavior you would expect + * from the defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.). + * + * @return a representation of a collection with the auctions that are open for bids + */ + @GetMapping(path = "/auctions/") + public ResponseEntity retrieveOpenAuctions() { + Collection auctions = + retrieveAuctionListUseCase.retrieveAuctions(new RetrieveOpenAuctionsQuery()); + + ObjectMapper mapper = new ObjectMapper(); + ArrayNode array = mapper.createArrayNode(); + + for (Auction auction : auctions) { + AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction); + JsonNode node = mapper.valueToTree(representation); + array.add(node); + } + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json"); + + // TODO before providing to students: remove hub links + responseHeaders.add(HttpHeaders.LINK, "; rel=\"hub\""); + responseHeaders.add(HttpHeaders.LINK, "; rel=\"self\""); + + return new ResponseEntity<>(array.toString(), responseHeaders, HttpStatus.OK); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java new file mode 100644 index 0000000..9e6ec67 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java @@ -0,0 +1,37 @@ +package ch.unisg.tapas.auctionhouse.adapter.out.messaging.websub; + +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent; +import ch.unisg.tapas.common.ConfigProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This class is a template for publishing auction started events via WebSub. + */ +@Component +@Primary +public class PublishAuctionStartedEventWebSubAdapter implements AuctionStartedEventPort { + // You can use this object to retrieve properties from application.properties, e.g. the + // WebSub hub publish endpoint, etc. + @Autowired + private ConfigProperties config; + + @Override + public void publishAuctionStartedEvent(AuctionStartedEvent event) { + // TODO + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java new file mode 100644 index 0000000..26949f2 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java @@ -0,0 +1,20 @@ +package ch.unisg.tapas.auctionhouse.adapter.out.web; + +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort; +import ch.unisg.tapas.auctionhouse.domain.AuctionWonEvent; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +/** + * This class is a template for sending auction won events via HTTP. This class was created here only + * as a placeholder, it is up to you to decide how such events should be sent (e.g., via HTTP, + * WebSub, etc.). + */ +@Component +@Primary +public class AuctionWonEventHttpAdapter implements AuctionWonEventPort { + @Override + public void publishAuctionWonEvent(AuctionWonEvent event) { + + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java new file mode 100644 index 0000000..6db8c68 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java @@ -0,0 +1,19 @@ +package ch.unisg.tapas.auctionhouse.adapter.out.web; + +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommandPort; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +/** + * This class is a tempalte for implementing a place bid for auction command via HTTP. + */ +@Component +@Primary +public class PlaceBidForAuctionCommandHttpAdapter implements PlaceBidForAuctionCommandPort { + + @Override + public void placeBid(PlaceBidForAuctionCommand command) { + // TODO + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java new file mode 100644 index 0000000..e4b312f --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java @@ -0,0 +1,59 @@ +package ch.unisg.tapas.auctionhouse.application.handler; + +import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEvent; +import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEventHandler; +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommandPort; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Bid; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import ch.unisg.tapas.common.ConfigProperties; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Handler for auction started events. This handler will automatically bid in any auction for a + * task of known type, i.e. a task for which the auction house knows an executor is available. + */ +@Component +public class AuctionStartedHandler implements AuctionStartedEventHandler { + private static final Logger LOGGER = LogManager.getLogger(AuctionStartedHandler.class); + + @Autowired + private ConfigProperties config; + + @Autowired + private PlaceBidForAuctionCommandPort placeBidForAuctionCommandPort; + + /** + * Handles an auction started event and bids in all auctions for tasks of known types. + * + * @param auctionStartedEvent the auction started domain event + * @return true unless a runtime exception occurs + */ + @Override + public boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent) { + Auction auction = auctionStartedEvent.getAuction(); + + if (ExecutorRegistry.getInstance().containsTaskType(auction.getTaskType())) { + LOGGER.info("Placing bid for task " + auction.getTaskUri() + " of type " + + auction.getTaskType() + " in auction " + auction.getAuctionId() + + " from auction house " + auction.getAuctionHouseUri().getValue().toString()); + + Bid bid = new Bid(auction.getAuctionId(), + new Bid.BidderName(config.getGroupName()), + new Bid.BidderAuctionHouseUri(config.getAuctionHouseUri()), + new Bid.BidderTaskListUri(config.getTaskListUri()) + ); + + PlaceBidForAuctionCommand command = new PlaceBidForAuctionCommand(auction, bid); + placeBidForAuctionCommandPort.placeBid(command); + } else { + LOGGER.info("Cannot execute this task type: " + auction.getTaskType().getValue()); + } + + return true; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java new file mode 100644 index 0000000..624e669 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java @@ -0,0 +1,16 @@ +package ch.unisg.tapas.auctionhouse.application.handler; + +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEventHandler; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import org.springframework.stereotype.Component; + +@Component +public class ExecutorAddedHandler implements ExecutorAddedEventHandler { + + @Override + public boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent) { + return ExecutorRegistry.getInstance().addExecutor(executorAddedEvent.getTaskType(), + executorAddedEvent.getExecutorId()); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java new file mode 100644 index 0000000..c3bfed8 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapas.auctionhouse.application.handler; + +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEvent; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEventHandler; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import org.springframework.stereotype.Component; + +/** + * Handler for executor removed events. It removes the executor from this auction house's executor + * registry. + */ +@Component +public class ExecutorRemovedHandler implements ExecutorRemovedEventHandler { + + @Override + public boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent) { + return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorId()); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java new file mode 100644 index 0000000..b937e26 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java @@ -0,0 +1,21 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Event that notifies this auction house that an auction was started by another auction house. + */ +@Value +public class AuctionStartedEvent extends SelfValidating { + @NotNull + private final Auction auction; + + public AuctionStartedEvent(Auction auction) { + this.auction = auction; + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java new file mode 100644 index 0000000..1eed1d9 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +public interface AuctionStartedEventHandler { + + boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java new file mode 100644 index 0000000..5a53b94 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java @@ -0,0 +1,32 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskType; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Event that notifies the auction house that an executor has been added to this TAPAS application. + */ +@Value +public class ExecutorAddedEvent extends SelfValidating { + @NotNull + private final ExecutorIdentifier executorId; + + @NotNull + private final AuctionedTaskType taskType; + + /** + * Constructs an executor added event. + * + * @param executorId the identifier of the executor that was added to this TAPAS application + */ + public ExecutorAddedEvent(ExecutorIdentifier executorId, AuctionedTaskType taskType) { + this.executorId = executorId; + this.taskType = taskType; + + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java new file mode 100644 index 0000000..ca82a1c --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +public interface ExecutorAddedEventHandler { + + boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java new file mode 100644 index 0000000..4d5c910 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java @@ -0,0 +1,26 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Event that notifies the auction house that an executor has been removed from this TAPAS application. + */ +@Value +public class ExecutorRemovedEvent extends SelfValidating { + @NotNull + private final ExecutorIdentifier executorId; + + /** + * Constructs an executor removed event. + * + * @param executorId the identifier of the executor that was removed from this TAPAS application + */ + public ExecutorRemovedEvent(ExecutorIdentifier executorId) { + this.executorId = executorId; + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java new file mode 100644 index 0000000..6d92422 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +public interface ExecutorRemovedEventHandler { + + boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java new file mode 100644 index 0000000..626fa49 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java @@ -0,0 +1,37 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Command for launching an auction in this auction house. + */ +@Value +public class LaunchAuctionCommand extends SelfValidating { + @NotNull + private final Auction.AuctionedTaskUri taskUri; + + @NotNull + private final Auction.AuctionedTaskType taskType; + + private final Auction.AuctionDeadline deadline; + + /** + * Constructs the launch action command. + * + * @param taskUri the URI of the auctioned task + * @param taskType the type of the auctioned task + * @param deadline the deadline by which the auction should receive bids (can be null if none) + */ + public LaunchAuctionCommand(Auction.AuctionedTaskUri taskUri, Auction.AuctionedTaskType taskType, + Auction.AuctionDeadline deadline) { + this.taskUri = taskUri; + this.taskType = taskType; + this.deadline = deadline; + + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java new file mode 100644 index 0000000..261240b --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java @@ -0,0 +1,8 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; + +public interface LaunchAuctionUseCase { + + Auction launchAuction(LaunchAuctionCommand command); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java new file mode 100644 index 0000000..a77f267 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java @@ -0,0 +1,7 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +/** + * Query used to retrieve open auctions. Although this query is empty, we model it to convey the + * domain semantics and to reduce coupling. + */ +public class RetrieveOpenAuctionsQuery { } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java new file mode 100644 index 0000000..4f94df2 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java @@ -0,0 +1,10 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; + +import java.util.Collection; + +public interface RetrieveOpenAuctionsUseCase { + + Collection retrieveAuctions(RetrieveOpenAuctionsQuery query); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java new file mode 100644 index 0000000..9a432c9 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java @@ -0,0 +1,11 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent; + +/** + * Port for sending out auction started events + */ +public interface AuctionStartedEventPort { + + void publishAuctionStartedEvent(AuctionStartedEvent event); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java new file mode 100644 index 0000000..7ed440f --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java @@ -0,0 +1,11 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +import ch.unisg.tapas.auctionhouse.domain.AuctionWonEvent; + +/** + * Port for sending out auction won events + */ +public interface AuctionWonEventPort { + + void publishAuctionWonEvent(AuctionWonEvent event); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java new file mode 100644 index 0000000..e207891 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java @@ -0,0 +1,25 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Bid; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Command to place a bid for a given auction. + */ +@Value +public class PlaceBidForAuctionCommand extends SelfValidating { + @NotNull + private final Auction auction; + @NotNull + private final Bid bid; + + public PlaceBidForAuctionCommand(Auction auction, Bid bid) { + this.auction = auction; + this.bid = bid; + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java new file mode 100644 index 0000000..3bf5a16 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +public interface PlaceBidForAuctionCommandPort { + + void placeBid(PlaceBidForAuctionCommand command); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java new file mode 100644 index 0000000..bdba393 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java @@ -0,0 +1,22 @@ +package ch.unisg.tapas.auctionhouse.application.service; + +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery; +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.AuctionRegistry; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +/** + * Service that implements {@link RetrieveOpenAuctionsUseCase} to retrieve all auctions in this auction + * house that are open for bids. + */ +@Component +public class RetrieveOpenAuctionsService implements RetrieveOpenAuctionsUseCase { + + @Override + public Collection retrieveAuctions(RetrieveOpenAuctionsQuery query) { + return AuctionRegistry.getInstance().getOpenAuctions(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java new file mode 100644 index 0000000..42c6e37 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java @@ -0,0 +1,113 @@ +package ch.unisg.tapas.auctionhouse.application.service; + +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase; +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort; +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort; +import ch.unisg.tapas.auctionhouse.domain.*; +import ch.unisg.tapas.common.ConfigProperties; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Service that implements the {@link LaunchAuctionUseCase} to start an auction. If a deadline is + * specified for the auction, the service automatically closes the auction at the deadline. If a + * deadline is not specified, the service closes the auction after 10s by default. + */ +@Component +public class StartAuctionService implements LaunchAuctionUseCase { + private static final Logger LOGGER = LogManager.getLogger(StartAuctionService.class); + + private final static int DEFAULT_AUCTION_DEADLINE_MILLIS = 10000; + + // Event port used to publish an auction started event + private final AuctionStartedEventPort auctionStartedEventPort; + // Event port used to publish an auction won event + private final AuctionWonEventPort auctionWonEventPort; + + private final ScheduledExecutorService service; + private final AuctionRegistry auctions; + + @Autowired + private ConfigProperties config; + + public StartAuctionService(AuctionStartedEventPort auctionStartedEventPort, + AuctionWonEventPort auctionWonEventPort) { + this.auctionStartedEventPort = auctionStartedEventPort; + this.auctionWonEventPort = auctionWonEventPort; + this.auctions = AuctionRegistry.getInstance(); + this.service = Executors.newScheduledThreadPool(1); + } + + /** + * Launches an auction. + * + * @param command the domain command used to launch the auction (see {@link LaunchAuctionCommand}) + * @return the launched auction + */ + @Override + public Auction launchAuction(LaunchAuctionCommand command) { + Auction.AuctionDeadline deadline = (command.getDeadline() == null) ? + new Auction.AuctionDeadline(DEFAULT_AUCTION_DEADLINE_MILLIS) : command.getDeadline(); + + // Create a new auction and add it to the auction registry + Auction auction = new Auction(new Auction.AuctionHouseUri(config.getAuctionHouseUri()), + command.getTaskUri(), command.getTaskType(), deadline); + auctions.addAuction(auction); + + // Schedule the closing of the auction at the deadline + service.schedule(new CloseAuctionTask(auction.getAuctionId()), deadline.getValue(), + TimeUnit.MILLISECONDS); + + // Publish an auction started event + AuctionStartedEvent auctionStartedEvent = new AuctionStartedEvent(auction); + auctionStartedEventPort.publishAuctionStartedEvent(auctionStartedEvent); + + return auction; + } + + /** + * This task closes the auction at the deadline and selects a winner if any bids were placed. It + * also sends out associated events and commands. + */ + private class CloseAuctionTask implements Runnable { + Auction.AuctionId auctionId; + + public CloseAuctionTask(Auction.AuctionId auctionId) { + this.auctionId = auctionId; + } + + @Override + public void run() { + Optional auctionOpt = auctions.getAuctionById(auctionId); + + if (auctionOpt.isPresent()) { + Auction auction = auctionOpt.get(); + Optional bid = auction.selectBid(); + + // Close the auction + auction.close(); + + if (bid.isPresent()) { + // Notify the bidder + Bid.BidderName bidderName = bid.get().getBidderName(); + LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " for task " + + auction.getTaskUri().getValue() + " won by " + bidderName.getValue()); + + // Send an auction won event for the winning bid + auctionWonEventPort.publishAuctionWonEvent(new AuctionWonEvent(bid.get())); + } else { + LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " ended with no bids for task " + + auction.getTaskUri().getValue()); + } + } + } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java new file mode 100644 index 0000000..3e51ef7 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java @@ -0,0 +1,171 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; +import lombok.Value; + +import java.net.URI; +import java.util.*; + +/** + * Domain entity that models an auction. + */ +public class Auction { + // Auctions have two possible states: + // - open: waiting for bids + // - closed: the auction deadline has expired, there may or may not be a winning bid + public enum Status { + OPEN, CLOSED + } + + // One way to generate auction identifiers is incremental starting from 1. This makes identifiers + // predictable, which can help with debugging when multiple parties are interacting, but it also + // means that auction identifiers are not universally unique unless they are part of a URI. + // An alternative would be to use UUIDs (see constructor). + private static long AUCTION_COUNTER = 1; + + @Getter + private AuctionId auctionId; + + @Getter + private AuctionStatus auctionStatus; + + // URI that identifies the auction house that started this auction. Given a uniform, standard + // HTTP API for auction houses, this URI can then be used as a base URI for interacting with + // the identified auction house. + @Getter + private final AuctionHouseUri auctionHouseUri; + + // URI that identifies the task for which the auction was launched. URIs are uniform identifiers + // and can be referenced independent of context: because we have defined a uniform HTTP API for + // TAPAS-Tasks, we can dereference this URI to retrieve a complete representation of the + // auctioned task. + @Getter + private final AuctionedTaskUri taskUri; + + // The type of the task being auctioned. We could also retrieve the task type by dereferencing + // the task's URI, but given that the bidding is defined primarily based on task types, knowing + // the task type avoids an additional HTTP request. + @Getter + private final AuctionedTaskType taskType; + + // The deadline by which bids can be placed. Once the deadline expires, the auction is closed. + @Getter + private final AuctionDeadline deadline; + + // Available bids. + @Getter + private final List bids; + + /** + * Constructs an auction. + * + * @param auctionHouseUri the URI of the auction hause that started the auction + * @param taskUri the URI of the task being auctioned + * @param taskType the type of the task being auctioned + * @param deadline the deadline by which the auction is open for bids + */ + public Auction(AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri, + AuctionedTaskType taskType, AuctionDeadline deadline) { + // Generates an incremental identifier + this.auctionId = new AuctionId("" + AUCTION_COUNTER ++); + // As an alternative, we could also generate an UUID + // this.auctionId = new AuctionId(UUID.randomUUID().toString()); + + this.auctionStatus = new AuctionStatus(Status.OPEN); + + this.auctionHouseUri = auctionHouseUri; + this.taskUri = taskUri; + this.taskType = taskType; + + this.deadline = deadline; + this.bids = new ArrayList<>(); + } + + /** + * Constructs an auction. + * + * @param auctionId the identifier of the auction + * @param auctionHouseUri the URI of the auction hause that started the auction + * @param taskUri the URI of the task being auctioned + * @param taskType the type of the task being auctioned + * @param deadline the deadline by which the auction is open for bids + */ + public Auction(AuctionId auctionId, AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri, + AuctionedTaskType taskType, AuctionDeadline deadline) { + this(auctionHouseUri, taskUri, taskType, deadline); + this.auctionId = auctionId; + } + + /** + * Places a bid for this auction. + * + * @param bid the bid + */ + public void addBid(Bid bid) { + bids.add(bid); + } + + /** + * Selects a bid randomly from the bids available for this auction. + * + * @return a winning bid or Optional.empty if no bid was made in this auction. + */ + public Optional selectBid() { + if (bids.isEmpty()) { + return Optional.empty(); + } + + int index = new Random().nextInt(bids.size()); + return Optional.of(bids.get(index)); + } + + /** + * Checks if the auction is open for bids. + * + * @return true if open for bids, false if the auction is closed + */ + public boolean isOpen() { + return auctionStatus.getValue() == Status.OPEN; + } + + /** + * Closes the auction. Called by the StartAuctionService after the auction deadline has expired. + */ + public void close() { + auctionStatus = new AuctionStatus(Status.CLOSED); + } + + /* + * Definitions of Value Objects + */ + + @Value + public static class AuctionId { + String value; + } + + @Value + public static class AuctionStatus { + Status value; + } + + @Value + public static class AuctionHouseUri { + URI value; + } + + @Value + public static class AuctionedTaskUri { + URI value; + } + + @Value + public static class AuctionedTaskType { + String value; + } + + @Value + public static class AuctionDeadline { + int value; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java new file mode 100644 index 0000000..858589d --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java @@ -0,0 +1,105 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import java.util.Collection; +import java.util.Hashtable; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Registry that keeps an in-memory history of auctions (both open for bids and closed). This class + * is a singleton. See also {@link Auction}. + */ +public class AuctionRegistry { + private static AuctionRegistry registry; + + private final Map auctions; + + private AuctionRegistry() { + this.auctions = new Hashtable<>(); + } + + /** + * Retrieves a reference to the auction registry. + * + * @return the auction registry + */ + public static synchronized AuctionRegistry getInstance() { + if (registry == null) { + registry = new AuctionRegistry(); + } + + return registry; + } + + /** + * Adds a new auction to the registry + * @param auction the new auction + */ + public void addAuction(Auction auction) { + auctions.put(auction.getAuctionId(), auction); + } + + /** + * Places a bid. See also {@link Bid}. + * + * @param bid the bid to be placed. + * @return false if the bid is for an auction with an unknown identifier, true otherwise + */ + public boolean placeBid(Bid bid) { + if (!containsAuctionWithId(bid.getAuctionId())) { + return false; + } + + Auction auction = getAuctionById(bid.getAuctionId()).get(); + auction.addBid(bid); + auctions.put(bid.getAuctionId(), auction); + + return true; + } + + /** + * Checks if the registry contains an auction with the given identifier. + * + * @param auctionId the auction's identifier + * @return true if the registry contains an auction with the given identifier, false otherwise + */ + public boolean containsAuctionWithId(Auction.AuctionId auctionId) { + return auctions.containsKey(auctionId); + } + + /** + * Retrieves the auction with the given identifier if it exists. + * + * @param auctionId the auction's identifier + * @return the auction or Optional.empty if the identifier is unknown + */ + public Optional getAuctionById(Auction.AuctionId auctionId) { + if (containsAuctionWithId(auctionId)) { + return Optional.of(auctions.get(auctionId)); + } + + return Optional.empty(); + } + + /** + * Retrieves all auctions in the registry. + * + * @return a collection with all auctions + */ + public Collection getAllAuctions() { + return auctions.values(); + } + + /** + * Retrieves only the auctions that are open for bids. + * + * @return a collection with all open auctions + */ + public Collection getOpenAuctions() { + return getAllAuctions() + .stream() + .filter(auction -> auction.isOpen()) + .collect(Collectors.toList()); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java new file mode 100644 index 0000000..7cac1ec --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java @@ -0,0 +1,15 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; + +/** + * A domain event that models an auction has started. + */ +public class AuctionStartedEvent { + @Getter + private Auction auction; + + public AuctionStartedEvent(Auction auction) { + this.auction = auction; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java new file mode 100644 index 0000000..484646c --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java @@ -0,0 +1,16 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; + +/** + * A domain event that models an auction was won. + */ +public class AuctionWonEvent { + // The winning bid + @Getter + private Bid winningBid; + + public AuctionWonEvent(Bid winningBid) { + this.winningBid = winningBid; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java new file mode 100644 index 0000000..4f24f30 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java @@ -0,0 +1,66 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; +import lombok.Value; + +import java.net.URI; + +/** + * Domain entity that models a bid. + */ +public class Bid { + // The identifier of the auction for which the bid is placed + @Getter + private final Auction.AuctionId auctionId; + + // The name of the bidder, i.e. the identifier of the TAPAS group + @Getter + private final BidderName bidderName; + + // URI that identifies the auction house of the bidder. Given a uniform, standard HTTP API for + // auction houses, this URI can then be used as a base URI for interacting with the auction house + // of the bidder. + @Getter + private final BidderAuctionHouseUri bidderAuctionHouseUri; + + // URI that identifies the TAPAS-Tasks task list of the bidder. Given a uniform, standard HTTP API + // for TAPAS-Tasks, this URI can then be used as a base URI for interacting with the list of tasks + // of the bidder, e.g. to delegate a task. + @Getter + private final BidderTaskListUri bidderTaskListUri; + + /** + * Constructs a bid. + * + * @param auctionId the identifier of the auction for which the bid is placed + * @param bidderName the name of the bidder, i.e. the identifier of the TAPAS group + * @param auctionHouseUri the URI of the bidder's auction house + * @param taskListUri the URI fo the bidder's list of tasks + */ + public Bid(Auction.AuctionId auctionId, BidderName bidderName, BidderAuctionHouseUri auctionHouseUri, + BidderTaskListUri taskListUri) { + this.auctionId = auctionId; + this.bidderName = bidderName; + this.bidderAuctionHouseUri = auctionHouseUri; + this.bidderTaskListUri = taskListUri; + } + + /* + * Definitions of Value Objects + */ + + @Value + public static class BidderName { + private String value; + } + + @Value + public static class BidderAuctionHouseUri { + private URI value; + } + + @Value + public static class BidderTaskListUri { + private URI value; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java new file mode 100644 index 0000000..9da3756 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java @@ -0,0 +1,86 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Value; + +import java.util.*; + +/** + * Registry that keeps a track of executors internal to the TAPAS application and the types of tasks + * they can achieve. One executor may correspond to multiple task types. This mapping is used when + * bidding for tasks: the auction house will only bid for tasks for which there is a known executor. + * This class is a singleton. + */ +public class ExecutorRegistry { + private static ExecutorRegistry registry; + + private final Map> executors; + + private ExecutorRegistry() { + this.executors = new Hashtable<>(); + } + + public static synchronized ExecutorRegistry getInstance() { + if (registry == null) { + registry = new ExecutorRegistry(); + } + + return registry; + } + + /** + * Adds an executor to the registry for a given task type. + * + * @param taskType the type of the task + * @param executorIdentifier the identifier of the executor (can be any string) + * @return true unless a runtime exception occurs + */ + public boolean addExecutor(Auction.AuctionedTaskType taskType, ExecutorIdentifier executorIdentifier) { + Set taskTypeExecs = executors.getOrDefault(taskType, + Collections.synchronizedSet(new HashSet<>())); + + taskTypeExecs.add(executorIdentifier); + executors.put(taskType, taskTypeExecs); + + return true; + } + + /** + * Removes an executor from the registry. The executor is disassociated from all known task types. + * + * @param executorIdentifier the identifier of the executor (can be any string) + * @return true unless a runtime exception occurs + */ + public boolean removeExecutor(ExecutorIdentifier executorIdentifier) { + Iterator iterator = executors.keySet().iterator(); + + while (iterator.hasNext()) { + Auction.AuctionedTaskType taskType = iterator.next(); + Set set = executors.get(taskType); + + set.remove(executorIdentifier); + + if (set.isEmpty()) { + iterator.remove(); + } + } + + return true; + } + + /** + * Checks if the registry contains an executor for a given task type. Used during an auction to + * decide if a bid should be placed. + * + * @param taskType the task type being auctioned + * @return + */ + public boolean containsTaskType(Auction.AuctionedTaskType taskType) { + return executors.containsKey(taskType); + } + + // Value Object for the executor identifier + @Value + public static class ExecutorIdentifier { + String value; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java new file mode 100644 index 0000000..c4809ef --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java @@ -0,0 +1,57 @@ +package ch.unisg.tapas.common; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; + +/** + * Class that wraps up the resource directory used to discover auction houses in Week 6. + */ +public class AuctionHouseResourceDirectory { + private final URI rdEndpoint; + + /** + * Constructs a resource directory for auction house given a known URI. + * + * @param rdEndpoint the based endpoint of the resource directory + */ + public AuctionHouseResourceDirectory(URI rdEndpoint) { + this.rdEndpoint = rdEndpoint; + } + + /** + * Retrieves the endpoints of all auctions houses registered with this directory. + * @return + */ + public List retrieveAuctionHouseEndpoints() { + List auctionHouseEndpoints = new ArrayList<>(); + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(rdEndpoint).GET().build(); + + HttpResponse response = HttpClient.newBuilder().build() + .send(request, HttpResponse.BodyHandlers.ofString()); + + // For simplicity, here we just hard code the current representation used by our + // resource directory for auction houses + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode payload = objectMapper.readTree(response.body()); + + for (JsonNode node : payload) { + auctionHouseEndpoints.add(node.get("endpoint").asText()); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + + return auctionHouseEndpoints; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java new file mode 100644 index 0000000..748afda --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java @@ -0,0 +1,64 @@ +package ch.unisg.tapas.common; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.net.URI; + +/** + * Used to access properties provided via application.properties + */ +@Component +public class ConfigProperties { + @Autowired + private Environment environment; + + /** + * Retrieves the URI of the WebSub hub. In this project, we use a single WebSub hub, but we could + * use multiple. + * + * @return the URI of the WebSub hub + */ + public URI getWebSubHub() { + return URI.create(environment.getProperty("websub.hub")); + } + + /** + * Retrieves the URI used to publish content via WebSub. In this project, we use a single + * WebSub hub, but we could use multiple. This URI is usually different from the WebSub hub URI. + * + * @return URI used to publish content via the WebSub hub + */ + public URI getWebSubPublishEndpoint() { + return URI.create(environment.getProperty("websub.hub.publish")); + } + + /** + * Retrieves the name of the group providing this auction house. + * + * @return the identifier of the group, e.g. tapas-group1 + */ + public String getGroupName() { + return environment.getProperty("group"); + } + + /** + * Retrieves the base URI of this auction house. + * + * @return the base URI of this auction house + */ + public URI getAuctionHouseUri() { + return URI.create(environment.getProperty("auction.house.uri")); + } + + /** + * Retrieves the URI of the TAPAS-Tasks task list of this TAPAS applicatoin. This is used, e.g., + * when placing a bid during the auction (see also {@link ch.unisg.tapas.auctionhouse.domain.Bid}). + * + * @return + */ + public URI getTaskListUri() { + return URI.create(environment.getProperty("tasks.list.uri")); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java new file mode 100644 index 0000000..1b56db4 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java @@ -0,0 +1,25 @@ +package ch.unisg.tapas.common; + +import javax.validation.*; +import java.util.Set; + +public class SelfValidating { + + private Validator validator; + + public SelfValidating() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + /** + * Evaluates all Bean Validations on the attributes of this + * instance. + */ + protected void validateSelf() { + Set> violations = validator.validate((T) this); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } +} diff --git a/tapas-auction-house/src/main/resources/application.properties b/tapas-auction-house/src/main/resources/application.properties new file mode 100644 index 0000000..2c92c87 --- /dev/null +++ b/tapas-auction-house/src/main/resources/application.properties @@ -0,0 +1,8 @@ +server.port=8082 + +websub.hub=https://websub.appspot.com/ +websub.hub.publish=https://websub.appspot.com/ + +group=tapas-group-tutors +auction.house.uri=https://tapas-auction-house.86-119-34-23.nip.io/ +tasks.list.uri=https://tapas-tasks.86-119-34-23.nip.io/ diff --git a/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java b/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java new file mode 100644 index 0000000..ce414c3 --- /dev/null +++ b/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java @@ -0,0 +1,13 @@ +package ch.unisg.tapas; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TapasAuctionHouseApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/tapas-tasks/README.md b/tapas-tasks/README.md index f083776..90016c3 100644 --- a/tapas-tasks/README.md +++ b/tapas-tasks/README.md @@ -11,61 +11,159 @@ with default editor settings. EditorConfig is supported out-of-the-box by the In consistent code styles, we recommend to reuse this editor configuration file in all your services. ## HTTP API Overview -The code we provide includes a minimalistic HTTP API for (i) creating a new task and (ii) retrieving -the representation of a task. +The code we provide includes a minimalistic uniform HTTP API for (i) creating a new task, (ii) retrieving +a representation of the current state of a task, and (iii) patching the representation of a task, which +is mapped to a domain/integration event. + +The representations exchanged with the API use two media types: +* a JSON-based format for task with the media type `application/task+json`; this media type is defined + in the context of our project, but could be [registered with IANA](https://www.iana.org/assignments/media-types) + to promote interoperability (see + [TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java) + for more details) +* the [JSON Patch](http://jsonpatch.com/) format with the registered media type `application/json-patch+json`, which is also a + JSON-based format (see sample HTTP requests below). For further developing and working with your HTTP API, we recommend to use [Postman](https://www.postman.com/). ### Creating a new task A new task is created via an `HTTP POST` request to the `/tasks/` endpoint. The body of the request -must include a JSON payload with the content type `application/json` and two required fields: +must include a representation of the task to be created using the content type `application/task+json` +defined in the context of this project. A valid representation must include at least two required fields +(see [TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java) +for more details): * `taskName`: a string that represents the name of the task to be created * `taskType`: a string that represents the type of the task to be created A sample HTTP request with `curl`: ```shell -curl -i --location --request POST 'http://localhost:8081/tasks/' --header 'Content-Type: application/json' --data-raw '{ - "taskName" : "task1", - "taskType" : "type1" +curl -i --location --request POST 'http://localhost:8081/tasks/' \ +--header 'Content-Type: application/task+json' \ +--data-raw '{ + "taskName" : "task1", + "taskType" : "computation", + "originalTaskUri" : "http://example.org", + "inputData" : "1+1" }' HTTP/1.1 201 -Content-Type: application/json -Content-Length: 142 -Date: Sun, 03 Oct 2021 17:25:32 GMT +Location: http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354 +Content-Type: application/task+json +Content-Length: 170 +Date: Sun, 17 Oct 2021 21:03:34 GMT { - "taskType" : "type1", - "taskState" : "OPEN", - "taskListName" : "tapas-tasks-tutors", - "taskName" : "task1", - "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"OPEN", + "originalTaskUri":"http://example.org", + "inputData":"1+1" } ``` -If the task is created successfuly, a `201 Created` status code is returned together with a JSON -representation of the created task. The representation includes, among others, a _universally unique -identifier (UUID)_ for the newly created task (`taskId`). +If the task is created successfuly, a `201 Created` status code is returned together with a +representation of the created task. The response also includes a `Location` header filed that points +to the URI of the newly created task. ### Retrieving a task -The representation of a task is retrieved via an `HTTP GET` request to the `/tasks/` endpoint. +The representation of a task is retrieved via an `HTTP GET` request to the URI of task. A sample HTTP request with `curl`: ```shell -curl -i --location --request GET 'http://localhost:8081/tasks/53cb19d6-2d9b-486f-98c7-c96c93b037f0' +curl -i --location --request GET 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' HTTP/1.1 200 -Content-Type: application/json -Content-Length: 142 -Date: Sun, 03 Oct 2021 17:27:06 GMT +Content-Type: application/task+json +Content-Length: 170 +Date: Sun, 17 Oct 2021 21:07:04 GMT { - "taskType" : "type1", - "taskState" : "OPEN", - "taskListName" : "tapas-tasks-tutors", - "taskName" : "task1", - "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"OPEN", + "originalTaskUri":"http://example.org", + "inputData":"1+1" } ``` + +### Patching a task + +REST emphasizes the generality of interfaces to promote uniform interaction. For instance, we can use +the `HTTP PATCH` method to implement fine-grained updates to the representational state of a task, which +may translate to various domain/integration events. However, to conform to the uniform interface +contraint in REST, any such updates have to rely on standard knowledge — and thus to hide away the +implementation details of our service. + +In addition to the `application/task+json` media type we defined for our uniform HTTP API, a standard +representation format we can use to specify fine-grained updates to the representation of tasks +is [JSON Patch](http://jsonpatch.com/). In what follow, we provide a few examples of `HTTP PATCH` requests. +For further details on the JSON Patch format, see also [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)). + +#### Changing the status of a task from OPEN to ASSIGNED + +Sample HTTP request that assigns the previously created task to group `tapas-group1`: + +```shell +curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \ +--header 'Content-Type: application/json-patch+json' \ +--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "ASSIGNED" }, + {"op" : "add", "path": "/serviceProvider", "value" : "tapas-group1" } ]' + +HTTP/1.1 200 +Content-Type: application/task+json +Content-Length: 207 +Date: Sun, 17 Oct 2021 21:20:58 GMT + +{ + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"ASSIGNED", + "originalTaskUri":"http://example.org", + "serviceProvider":"tapas-group1", + "inputData":"1+1" +} +``` + +In this example, the requested patch includes two JSON Patch operations: +* an operation to `replace` the `taskStatus` already in the task's representation with the value `ASSIGNED` +* an operation to `add` to the task's representation a `serviceProvider` with the value `tapas-group1` + +Internally, this request is mapped to a +[TaskAssignedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java). +The HTTP response returns a `200 OK` status code together with the updated representation of the task. + +#### Changing the status of a task from to EXECUTED + +Sample HTTP request that changes the status of the task to `EXECUTED` and adds an output result: + +```shell +curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \ +--header 'Content-Type: application/json-patch+json' \ +--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "EXECUTED" }, + {"op" : "add", "path": "/outputData", "value" : "2" } ]' + +HTTP/1.1 200 +Content-Type: application/task+json +Content-Length: 224 +Date: Sun, 17 Oct 2021 21:32:25 GMT + +{ + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"EXECUTED", + "originalTaskUri":"http://example.org", + "serviceProvider":"tapas-group1", + "inputData":"1+1", + "outputData":"2" +} +``` + +Internally, this request is mapped to a +[TaskExecutedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java). +The HTTP response returns a `200 OK` status code together with the updated representation of the task. diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml index a5b6587..3eac732 100644 --- a/tapas-tasks/pom.xml +++ b/tapas-tasks/pom.xml @@ -16,6 +16,12 @@ 11 + + + Eclipse Paho Repo + https://repo.eclipse.org/content/repositories/paho-releases/ + + org.springframework.boot @@ -49,17 +55,16 @@ 1.1.0.Final - - org.json - json - 20210307 - com.github.java-json-tools json-patch 1.13 - + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.0 + diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java index 40fa5da..2675391 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java @@ -3,13 +3,10 @@ package ch.unisg.tapastasks; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import java.util.Collections; - @SpringBootApplication public class TapasTasksApplication { public static void main(String[] args) { - SpringApplication tapasTasksApp = new SpringApplication(TapasTasksApplication.class); tapasTasksApp.run(args); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java new file mode 100644 index 0000000..94c1f47 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java @@ -0,0 +1,102 @@ +package ch.unisg.tapastasks.tasks.adapter.in.formats; + +import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * This class is used to process JSON Patch operations for tasks: given a + * JSON Patch for updating the representational state of a task, + * this class provides methods for extracting various operations of interest for our domain (e.g., + * changing the status of a task). + */ +public class TaskJsonPatchRepresentation { + public static final String MEDIA_TYPE = "application/json-patch+json"; + + private final JsonNode patch; + + /** + * Constructs the JSON Patch representation. + * + * @param patch a JSON Patch as JsonNode + */ + public TaskJsonPatchRepresentation(JsonNode patch) { + this.patch = patch; + } + + /** + * Extracts the first task status replaced in this patch. + * + * @return the first task status changed in this patch or an empty {@link Optional} if none is + * found + */ + public Optional extractFirstTaskStatusChange() { + Optional status = extractFirst(node -> + isPatchReplaceOperation(node) && hasPath(node, "/taskStatus") + ); + + if (status.isPresent()) { + String taskStatus = status.get().get("value").asText(); + return Optional.of(Task.Status.valueOf(taskStatus)); + } + + return Optional.empty(); + } + + /** + * Extracts the first service provider added or replaced in this patch. + * + * @return the first service provider changed in this patch or an empty {@link Optional} if none + * is found + */ + public Optional extractFirstServiceProviderChange() { + Optional serviceProvider = extractFirst(node -> + (isPatchReplaceOperation(node) || isPatchAddOperation(node)) + && hasPath(node, "/serviceProvider") + ); + + return (serviceProvider.isEmpty()) ? Optional.empty() + : Optional.of(new Task.ServiceProvider(serviceProvider.get().get("value").asText())); + } + + /** + * Extracts the first output data addition in this patch. + * + * @return the output data added in this patch or an empty {@link Optional} if none is found + */ + public Optional extractFirstOutputDataAddition() { + Optional output = extractFirst(node -> + isPatchAddOperation(node) && hasPath(node, "/outputData") + ); + + return (output.isEmpty()) ? Optional.empty() + : Optional.of(new Task.OutputData(output.get().get("value").asText())); + } + + private Optional extractFirst(Predicate predicate) { + Stream stream = StreamSupport.stream(patch.spliterator(), false); + return stream.filter(predicate).findFirst(); + } + + private boolean isPatchAddOperation(JsonNode node) { + return isPatchOperationOfType(node, "add"); + } + + private boolean isPatchReplaceOperation(JsonNode node) { + return isPatchOperationOfType(node, "replace"); + } + + private boolean isPatchOperationOfType(JsonNode node, String operation) { + return node.isObject() && node.get("op") != null + && node.get("op").asText().equalsIgnoreCase(operation); + } + + private boolean hasPath(JsonNode node, String path) { + return node.isObject() && node.get("path") != null + && node.get("path").asText().equalsIgnoreCase(path); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java new file mode 100644 index 0000000..eb89415 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java @@ -0,0 +1,115 @@ +package ch.unisg.tapastasks.tasks.adapter.in.formats; + +import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; + +/** + * This class is used to expose and consume representations of tasks through the HTTP interface. The + * representations conform to the custom JSON-based media type "application/task+json". The media type + * is just an identifier and can be registered with + * IANA to promote interoperability. + */ +final public class TaskJsonRepresentation { + // The media type used for this task representation format + public static final String MEDIA_TYPE = "application/task+json"; + + // A task identifier specific to our implementation (e.g., a UUID). This identifier is then used + // to generate the task's URI. URIs are standard uniform identifiers and use a universal syntax + // that can be referenced (and dereferenced) independent of context. In our uniform HTTP API, + // we identify tasks via URIs and not implementation-specific identifiers. + @Getter @Setter + private String taskId; + + // A string that represents the task's name + @Getter + private final String taskName; + + // A string that identifies the task's type. This string could also be a URI (e.g., defined in some + // Web ontology, as we shall see later in the course), but it's not constrained to be a URI. + // The task's type can be used to assign executors to tasks, to decide what tasks to bid for, etc. + @Getter + private final String taskType; + + // The task's status: OPEN, ASSIGNED, RUNNING, or EXECUTED (see Task.Status) + @Getter @Setter + private String taskStatus; + + // If this task is a delegated task (i.e., a shadow of another task), this URI points to the + // original task. Because URIs are standard and uniform, we can just dereference this URI to + // retrieve a representation of the original task. + @Getter @Setter + private String originalTaskUri; + + // The service provider who executes this task. The service provider is a any string that identifies + // a TAPAS group (e.g., tapas-group1). This identifier could also be a URI (if we have a good reason + // for it), but it's not constrained to be a URI. + @Getter @Setter + private String serviceProvider; + + // A string that provides domain-specific input data for this task. In the context of this project, + // we can parse and interpret the input data based on the task's type. + @Getter @Setter + private String inputData; + + // A string that provides domain-specific output data for this task. In the context of this project, + // we can parse and interpret the output data based on the task's type. + @Getter @Setter + private String outputData; + + /** + * Instantiate a task representation with a task name and type. + * + * @param taskName string that represents the task's name + * @param taskType string that represents the task's type + */ + public TaskJsonRepresentation(String taskName, String taskType) { + this.taskName = taskName; + this.taskType = taskType; + + this.taskStatus = null; + this.originalTaskUri = null; + this.serviceProvider = null; + this.inputData = null; + this.outputData = null; + } + + /** + * Instantiate a task representation from a domain concept. + * + * @param task the task + */ + public TaskJsonRepresentation(Task task) { + this(task.getTaskName().getValue(), task.getTaskType().getValue()); + + this.taskId = task.getTaskId().getValue(); + this.taskStatus = task.getTaskStatus().getValue().name(); + + this.originalTaskUri = (task.getOriginalTaskUri() == null) ? + null : task.getOriginalTaskUri().getValue(); + + this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue(); + this.inputData = (task.getInputData() == null) ? null : task.getInputData().getValue(); + this.outputData = (task.getOutputData() == null) ? null : task.getOutputData().getValue(); + } + + /** + * Convenience method used to serialize a task provided as a domain concept in the format exposed + * through the uniform HTTP API. + * + * @param task the task as defined in the domain + * @return a string serialization using the JSON-based representation format defined for tasks + * @throws JsonProcessingException if a runtime exception occurs during object serialization + */ + public static String serialize(Task task) throws JsonProcessingException { + TaskJsonRepresentation representation = new TaskJsonRepresentation(task); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return mapper.writeValueAsString(representation); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java new file mode 100644 index 0000000..fbeb7b7 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java @@ -0,0 +1,3 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging; + +public class UnknownEventException extends RuntimeException { } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java new file mode 100644 index 0000000..4c26b80 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java @@ -0,0 +1,39 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.application.handler.TaskAssignedHandler; +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.Task.TaskId; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; + +/** + * Listener for task assigned events. A task assigned event corresponds to a JSON Patch that attempts + * to change the task's status to ASSIGNED and may also add/replace a service provider (i.e., to what + * group the task was assigned). This implementation does not impose that a task assigned event + * includes the service provider (i.e., can be null). + * + * See also {@link TaskAssignedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}. + */ +public class TaskAssignedEventListenerHttpAdapter extends TaskEventListener { + + /** + * Handles the task assigned event. + * + * @param taskId the identifier of the task for which an event was received + * @param payload the JSON Patch payload of the HTTP PATCH request received for this task + * @return + */ + public Task handleTaskEvent(String taskId, JsonNode payload) { + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + Optional serviceProvider = representation.extractFirstServiceProviderChange(); + + TaskAssignedEvent taskAssignedEvent = new TaskAssignedEvent(new TaskId(taskId), serviceProvider); + TaskAssignedEventHandler taskAssignedEventHandler = new TaskAssignedHandler(); + + return taskAssignedEventHandler.handleTaskAssigned(taskAssignedEvent); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java new file mode 100644 index 0000000..940d6fa --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java @@ -0,0 +1,103 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; +import ch.unisg.tapastasks.tasks.adapter.in.messaging.UnknownEventException; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jsonpatch.JsonPatch; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.Optional; + + +/** + * This REST Controller handles HTTP PATCH requests for updating the representational state of Task + * resources. Each request to update the representational state of a Task resource can correspond to + * at most one domain/integration event. Request payloads use the + * JSON PATCH format and media type. + * + * A JSON Patch can contain multiple operations (e.g., add, remove, replace) for updating various + * parts of a task's representations. One or more JSON Patch operations can represent a domain/integration + * event. Therefore, the events can only be determined by inspecting the requested patch (e.g., a request + * to change a task's status from RUNNING to EXECUTED). This class is responsible to inspect requested + * patches, identify events, and to route them to appropriate listeners. + * + * For more details on JSON Patch, see: http://jsonpatch.com/ + * For some sample HTTP requests, see the README. + */ +@RestController +public class TaskEventHttpDispatcher { + // The standard media type for JSON Patch registered with IANA + // See: https://www.iana.org/assignments/media-types/application/json-patch+json + private final static String JSON_PATCH_MEDIA_TYPE = "application/json-patch+json"; + + /** + * Handles HTTP PATCH requests with a JSON Patch payload. Routes the requests based on the + * the operations requested in the patch. In this implementation, one HTTP Patch request is + * mapped to at most one domain event. + * + * @param taskId the local (i.e., implementation-specific) identifier of the task to the patched; + * this identifier is extracted from the task's URI + * @param payload the reuqested patch for this task + * @return 200 OK and a representation of the task after processing the event; 404 Not Found if + * the request URI does not match any task; 400 Bad Request if the request is invalid + */ + @PatchMapping(path = "/tasks/{taskId}", consumes = {JSON_PATCH_MEDIA_TYPE}) + public ResponseEntity dispatchTaskEvents(@PathVariable("taskId") String taskId, + @RequestBody JsonNode payload) { + try { + // Throw an exception if the JSON Patch format is invalid. This call is only used to + // validate the JSON PATCH syntax. + JsonPatch.fromJson(payload); + + // Check for known events and route the events to appropriate listeners + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + Optional status = representation.extractFirstTaskStatusChange(); + + TaskEventListener listener = null; + + // Route events related to task status changes + if (status.isPresent()) { + switch (status.get()) { + case ASSIGNED: + listener = new TaskAssignedEventListenerHttpAdapter(); + break; + case RUNNING: + listener = new TaskStartedEventListenerHttpAdapter(); + break; + case EXECUTED: + listener = new TaskExecutedEventListenerHttpAdapter(); + break; + } + } + + if (listener == null) { + // The HTTP PATCH request is valid, but the patch does not match any known event + throw new UnknownEventException(); + } + + Task task = listener.handleTaskEvent(taskId, payload); + + // Add the content type as a response header + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); + + return new ResponseEntity<>(TaskJsonRepresentation.serialize(task), responseHeaders, + HttpStatus.OK); + } catch (TaskNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } catch (IOException | RuntimeException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java new file mode 100644 index 0000000..8912968 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java @@ -0,0 +1,24 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Abstract class that handles events specific to a Task. Events are received via an HTTP PATCH + * request for a given task and dispatched to Task event listeners (see {@link TaskEventHttpDispatcher}). + * Each listener must implement the abstract method {@link #handleTaskEvent(String, JsonNode)}, which + * may require additional event-specific validations. + */ +public abstract class TaskEventListener { + + /** + * This abstract method handles a task event and returns the task after the event was handled. + * + * @param taskId the identifier of the task for which an event was received + * @param payload the JSON Patch payload of the HTTP PATCH request received for this task + * @return the task for which the HTTP PATCH request is handled + * @throws TaskNotFoundException + */ + public abstract Task handleTaskEvent(String taskId, JsonNode payload) throws TaskNotFoundException; +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java new file mode 100644 index 0000000..f1db541 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java @@ -0,0 +1,34 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.application.handler.TaskExecutedHandler; +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; + +/** + * Listener for task executed events. A task executed event corresponds to a JSON Patch that attempts + * to change the task's status to EXECUTED, may add/replace a service provider, and may also add an + * output result. This implementation does not impose that a task executed event includes either the + * service provider or an output result (i.e., both can be null). + * + * See also {@link TaskExecutedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}. + */ +public class TaskExecutedEventListenerHttpAdapter extends TaskEventListener { + + public Task handleTaskEvent(String taskId, JsonNode payload) { + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + + Optional serviceProvider = representation.extractFirstServiceProviderChange(); + Optional outputData = representation.extractFirstOutputDataAddition(); + + TaskExecutedEvent taskExecutedEvent = new TaskExecutedEvent(new Task.TaskId(taskId), + serviceProvider, outputData); + TaskExecutedEventHandler taskExecutedEventHandler = new TaskExecutedHandler(); + + return taskExecutedEventHandler.handleTaskExecuted(taskExecutedEvent); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java new file mode 100644 index 0000000..aa2f6b4 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java @@ -0,0 +1,32 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.application.handler.TaskStartedHandler; +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.Task.TaskId; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; + +/** + * Listener for task started events. A task started event corresponds to a JSON Patch that attempts + * to change the task's status to RUNNING and may also add/replace a service provider. This + * implementation does not impose that a task started event includes the service provider (i.e., + * can be null). + * + * See also {@link TaskStartedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}. + */ +public class TaskStartedEventListenerHttpAdapter extends TaskEventListener { + + public Task handleTaskEvent(String taskId, JsonNode payload) { + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + Optional serviceProvider = representation.extractFirstServiceProviderChange(); + + TaskStartedEvent taskStartedEvent = new TaskStartedEvent(new TaskId(taskId), serviceProvider); + TaskStartedEventHandler taskStartedEventHandler = new TaskStartedHandler(); + + return taskStartedEventHandler.handleTaskStarted(taskStartedEvent); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java index 53bebc1..234dcde 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java @@ -1,8 +1,12 @@ package ch.unisg.tapastasks.tasks.adapter.in.web; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand; import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase; import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -12,29 +16,67 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import javax.validation.ConstraintViolationException; +import java.util.Optional; +/** + * Controller that handles HTTP requests for creating new tasks. This controller implements the + * {@link AddNewTaskToTaskListUseCase} use case using the {@link AddNewTaskToTaskListCommand}. + * + * A new task is created via an HTTP POST request to the /tasks/ endpoint. The body of the request + * contains a JSON-based representation with the "application/task+json" media type defined for this + * project. This custom media type allows to capture the semantics of our JSON representations for + * tasks. + * + * If the request is successful, the controller returns an HTTP 201 Created status code and a + * representation of the created task with Content-Type "application/task+json". The HTTP response + * also include a Location header field that points to the URI of the created task. + */ @RestController public class AddNewTaskToTaskListWebController { private final AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase; + // Used to retrieve properties from application.properties + @Autowired + private Environment environment; + public AddNewTaskToTaskListWebController(AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase) { this.addNewTaskToTaskListUseCase = addNewTaskToTaskListUseCase; } - @PostMapping(path = "/tasks/", consumes = {TaskMediaType.TASK_MEDIA_TYPE}) - public ResponseEntity addNewTaskTaskToTaskList(@RequestBody Task task) { + @PostMapping(path = "/tasks/", consumes = {TaskJsonRepresentation.MEDIA_TYPE}) + public ResponseEntity addNewTaskTaskToTaskList(@RequestBody TaskJsonRepresentation payload) { try { - AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand( - task.getTaskName(), task.getTaskType() - ); + Task.TaskName taskName = new Task.TaskName(payload.getTaskName()); + Task.TaskType taskType = new Task.TaskType(payload.getTaskType()); - Task newTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command); + // If the created task is a delegated task, the representation contains a URI reference + // to the original task + Optional originalTaskUriOptional = + (payload.getOriginalTaskUri() == null) ? Optional.empty() + : Optional.of(new Task.OriginalTaskUri(payload.getOriginalTaskUri())); + + AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(taskName, taskType, + originalTaskUriOptional); + + Task createdTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command); + + // When creating a task, the task's representation may include optional input data + if (payload.getInputData() != null) { + createdTask.setInputData(new Task.InputData(payload.getInputData())); + } // Add the content type as a response header HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); + // Construct and advertise the URI of the newly created task; we retrieve the base URI + // from the application.properties file + responseHeaders.add(HttpHeaders.LOCATION, environment.getProperty("baseuri") + + "tasks/" + createdTask.getTaskId().getValue()); - return new ResponseEntity<>(TaskMediaType.serialize(newTask), responseHeaders, HttpStatus.CREATED); + return new ResponseEntity<>(TaskJsonRepresentation.serialize(createdTask), responseHeaders, + HttpStatus.CREATED); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } catch (ConstraintViolationException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java index 0eb6bea..d60e4d1 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java @@ -1,8 +1,10 @@ package ch.unisg.tapastasks.tasks.adapter.in.web; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListQuery; import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase; import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,6 +13,11 @@ import org.springframework.web.server.ResponseStatusException; import java.util.Optional; +/** + * Controller that handles HTTP GET requests for retrieving tasks. This controller implements the + * {@link RetrieveTaskFromTaskListUseCase} use case using the {@link RetrieveTaskFromTaskListQuery} + * query. + */ @RestController public class RetrieveTaskFromTaskListWebController { private final RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase; @@ -19,10 +26,17 @@ public class RetrieveTaskFromTaskListWebController { this.retrieveTaskFromTaskListUseCase = retrieveTaskFromTaskListUseCase; } + /** + * Retrieves a representation of task. Returns HTTP 200 OK if the request is successful with a + * representation of the task using the Content-Type "applicatoin/task+json". + * + * @param taskId the local identifier of the requested task (extracted from the task's URI) + * @return a representation of the task if the task exists + */ @GetMapping(path = "/tasks/{taskId}") public ResponseEntity retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) { - RetrieveTaskFromTaskListQuery command = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId)); - Optional updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(command); + RetrieveTaskFromTaskListQuery query = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId)); + Optional updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(query); // Check if the task with the given identifier exists if (updatedTaskOpt.isEmpty()) { @@ -30,11 +44,16 @@ public class RetrieveTaskFromTaskListWebController { throw new ResponseStatusException(HttpStatus.NOT_FOUND); } - // Add the content type as a response header - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + try { + String taskRepresentation = TaskJsonRepresentation.serialize(updatedTaskOpt.get()); - return new ResponseEntity<>(TaskMediaType.serialize(updatedTaskOpt.get()), responseHeaders, - HttpStatus.OK); + // Add the content type as a response header + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); + + return new ResponseEntity<>(taskRepresentation, responseHeaders, HttpStatus.OK); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java deleted file mode 100644 index 3c555e5..0000000 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java +++ /dev/null @@ -1,23 +0,0 @@ -package ch.unisg.tapastasks.tasks.adapter.in.web; - -import ch.unisg.tapastasks.tasks.domain.Task; -import ch.unisg.tapastasks.tasks.domain.TaskList; -import org.json.JSONObject; - -final public class TaskMediaType { - public static final String TASK_MEDIA_TYPE = "application/json"; - - public static String serialize(Task task) { - JSONObject payload = new JSONObject(); - - payload.put("taskId", task.getTaskId().getValue()); - payload.put("taskName", task.getTaskName().getValue()); - payload.put("taskType", task.getTaskType().getValue()); - payload.put("taskState", task.getTaskState().getValue()); - payload.put("taskListName", TaskList.getTapasTaskList().getTaskListName().getValue()); - - return payload.toString(); - } - - private TaskMediaType() { } -} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java new file mode 100644 index 0000000..7deb844 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapastasks.tasks.application.handler; + +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class TaskAssignedHandler implements TaskAssignedEventHandler { + + @Override + public Task handleTaskAssigned(TaskAssignedEvent taskAssignedEvent) throws TaskNotFoundException { + TaskList taskList = TaskList.getTapasTaskList(); + return taskList.changeTaskStatusToAssigned(taskAssignedEvent.getTaskId(), + taskAssignedEvent.getServiceProvider()); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java new file mode 100644 index 0000000..ec21e8c --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapastasks.tasks.application.handler; + +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class TaskExecutedHandler implements TaskExecutedEventHandler { + + @Override + public Task handleTaskExecuted(TaskExecutedEvent taskExecutedEvent) throws TaskNotFoundException { + TaskList taskList = TaskList.getTapasTaskList(); + return taskList.changeTaskStatusToExecuted(taskExecutedEvent.getTaskId(), + taskExecutedEvent.getServiceProvider(), taskExecutedEvent.getOutputData()); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java new file mode 100644 index 0000000..758be0b --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapastasks.tasks.application.handler; + +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class TaskStartedHandler implements TaskStartedEventHandler { + + @Override + public Task handleTaskStarted(TaskStartedEvent taskStartedEvent) throws TaskNotFoundException { + TaskList taskList = TaskList.getTapasTaskList(); + return taskList.changeTaskStatusToRunning(taskStartedEvent.getTaskId(), + taskStartedEvent.getServiceProvider()); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java index a0e0fec..fbb66ed 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java @@ -1,23 +1,30 @@ package ch.unisg.tapastasks.tasks.application.port.in; import ch.unisg.tapastasks.common.SelfValidating; -import ch.unisg.tapastasks.tasks.domain.Task.TaskType; -import ch.unisg.tapastasks.tasks.domain.Task.TaskName; +import ch.unisg.tapastasks.tasks.domain.Task; +import lombok.Getter; import lombok.Value; import javax.validation.constraints.NotNull; +import java.util.Optional; @Value public class AddNewTaskToTaskListCommand extends SelfValidating { @NotNull - private final TaskName taskName; + private final Task.TaskName taskName; @NotNull - private final TaskType taskType; + private final Task.TaskType taskType; - public AddNewTaskToTaskListCommand(TaskName taskName, TaskType taskType) { + @Getter + private final Optional originalTaskUri; + + public AddNewTaskToTaskListCommand(Task.TaskName taskName, Task.TaskType taskType, + Optional originalTaskUri) { this.taskName = taskName; this.taskType = taskType; + this.originalTaskUri = originalTaskUri; + this.validateSelf(); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java index 40afc1d..cf7d787 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java @@ -5,5 +5,5 @@ import ch.unisg.tapastasks.tasks.domain.Task; import java.util.Optional; public interface RetrieveTaskFromTaskListUseCase { - Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command); + Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java new file mode 100644 index 0000000..c58d034 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java @@ -0,0 +1,25 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task; +import lombok.Getter; +import lombok.Value; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Value +public class TaskAssignedEvent extends SelfValidating { + @NotNull + private final Task.TaskId taskId; + + @Getter + private final Optional serviceProvider; + + public TaskAssignedEvent(Task.TaskId taskId, Optional serviceProvider) { + this.taskId = taskId; + this.serviceProvider = serviceProvider; + + this.validateSelf(); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java new file mode 100644 index 0000000..67f78dd --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java @@ -0,0 +1,8 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +public interface TaskAssignedEventHandler { + + Task handleTaskAssigned(TaskAssignedEvent taskStartedEvent); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java new file mode 100644 index 0000000..7ed9c84 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java @@ -0,0 +1,34 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task.*; +import lombok.Getter; +import lombok.Value; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Value +public class TaskExecutedEvent extends SelfValidating { + @NotNull + private final TaskId taskId; + + @Getter + private final Optional serviceProvider; + + @Getter + private final Optional outputData; + + public TaskExecutedEvent(TaskId taskId, Optional serviceProvider, + Optional outputData) { + this.taskId = taskId; + + this.serviceProvider = serviceProvider; + this.outputData = outputData; + + this.validateSelf(); + } + + + +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java new file mode 100644 index 0000000..c1a18dc --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java @@ -0,0 +1,8 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +public interface TaskExecutedEventHandler { + + Task handleTaskExecuted(TaskExecutedEvent taskExecutedEvent); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java new file mode 100644 index 0000000..8fad698 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java @@ -0,0 +1,28 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task; +import lombok.Getter; +import lombok.Value; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Value +public class TaskStartedEvent extends SelfValidating { + @NotNull + private final Task.TaskId taskId; + + @Getter + private final Optional serviceProvider; + + public TaskStartedEvent(Task.TaskId taskId, Optional serviceProvider) { + this.taskId = taskId; + this.serviceProvider = serviceProvider; + + this.validateSelf(); + } + + + +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java new file mode 100644 index 0000000..0da730e --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java @@ -0,0 +1,8 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +public interface TaskStartedEventHandler { + + Task handleTaskStarted(TaskStartedEvent taskStartedEvent); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java index 48c75a6..2380fcf 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java @@ -21,7 +21,13 @@ public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase @Override public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) { TaskList taskList = TaskList.getTapasTaskList(); - Task newTask = taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType()); + + Task newTask = (command.getOriginalTaskUri().isPresent()) ? + // Create a delegated task that points back to the original task + taskList.addNewTaskWithNameAndTypeAndOriginalTaskUri(command.getTaskName(), + command.getTaskType(), command.getOriginalTaskUri().get()) + // Create an original task + : taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType()); //Here we are using the application service to emit the domain event to the outside of the bounded context. //This event should be considered as a light-weight "integration event" to communicate with other services. diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java index 46043b0..fd6aea5 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java @@ -15,8 +15,8 @@ import java.util.Optional; @Transactional public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase { @Override - public Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command) { + public Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query) { TaskList taskList = TaskList.getTapasTaskList(); - return taskList.retrieveTaskById(command.getTaskId()); + return taskList.retrieveTaskById(query.getTaskId()); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java index 0dcafc3..b664a64 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java @@ -8,7 +8,7 @@ import java.util.UUID; /**This is a domain entity**/ public class Task { - public enum State { + public enum Status { OPEN, ASSIGNED, RUNNING, EXECUTED } @@ -22,38 +22,81 @@ public class Task { private final TaskType taskType; @Getter - private TaskState taskState; + private final OriginalTaskUri originalTaskUri; + + @Getter @Setter + private TaskStatus taskStatus; + + @Getter @Setter + private ServiceProvider provider; + + @Getter @Setter + private InputData inputData; + + @Getter @Setter + private OutputData outputData; + + public Task(TaskName taskName, TaskType taskType, OriginalTaskUri taskUri) { + this.taskId = new TaskId(UUID.randomUUID().toString()); - public Task(TaskName taskName, TaskType taskType) { this.taskName = taskName; this.taskType = taskType; - this.taskState = new TaskState(State.OPEN); - this.taskId = new TaskId(UUID.randomUUID().toString()); + this.originalTaskUri = taskUri; + + this.taskStatus = new TaskStatus(Status.OPEN); + + this.inputData = null; + this.outputData = null; } protected static Task createTaskWithNameAndType(TaskName name, TaskType type) { //This is a simple debug message to see that the request has reached the right method in the core System.out.println("New Task: " + name.getValue() + " " + type.getValue()); - return new Task(name,type); + return new Task(name, type, null); + } + + protected static Task createTaskWithNameAndTypeAndOriginalTaskUri(TaskName name, TaskType type, + OriginalTaskUri originalTaskUri) { + return new Task(name, type, originalTaskUri); } @Value public static class TaskId { - private String value; + String value; } @Value public static class TaskName { - private String value; - } - - @Value - public static class TaskState { - private State value; + String value; } @Value public static class TaskType { - private String value; + String value; + } + + @Value + public static class OriginalTaskUri { + String value; + } + + @Value + public static class TaskStatus { + Status value; + } + + @Value + public static class ServiceProvider { + String value; + } + + @Value + public static class InputData { + String value; + } + + @Value + public static class OutputData { + String value; } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java index 2b90da5..4a52c62 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java @@ -3,7 +3,6 @@ package ch.unisg.tapastasks.tasks.domain; import lombok.Getter; import lombok.Value; -import javax.swing.text.html.Option; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -20,7 +19,7 @@ public class TaskList { //Note: We do not care about the management of task lists, there is only one within this service //--> using the Singleton pattern here to make lives easy; we will later load it from a repo - //TODO change "tutors" to your group name ("groupx") + //TODO: change "tutors" to your group name ("groupx") private static final TaskList taskList = new TaskList(new TaskListName("tapas-tasks-tutors")); private TaskList(TaskListName taskListName) { @@ -35,14 +34,27 @@ public class TaskList { //Only the aggregate root is allowed to create new tasks and add them to the task list. //Note: Here we could add some sophisticated invariants/business rules that the aggregate root checks public Task addNewTaskWithNameAndType(Task.TaskName name, Task.TaskType type) { - Task newTask = Task.createTaskWithNameAndType(name,type); - listOfTasks.value.add(newTask); - //This is a simple debug message to see that the task list is growing with each new request - System.out.println("Number of tasks: "+listOfTasks.value.size()); + Task newTask = Task.createTaskWithNameAndType(name, type); + this.addNewTaskToList(newTask); + + return newTask; + } + + public Task addNewTaskWithNameAndTypeAndOriginalTaskUri(Task.TaskName name, Task.TaskType type, + Task.OriginalTaskUri originalTaskUri) { + Task newTask = Task.createTaskWithNameAndTypeAndOriginalTaskUri(name, type, originalTaskUri); + this.addNewTaskToList(newTask); + + return newTask; + } + + private void addNewTaskToList(Task newTask) { //Here we would also publish a domain event to other entities in the core interested in this event. //However, we skip this here as it makes the core even more complex (e.g., we have to implement a light-weight //domain event publisher and subscribers (see "Implementing Domain-Driven Design by V. Vernon, pp. 296ff). - return newTask; + listOfTasks.value.add(newTask); + //This is a simple debug message to see that the task list is growing with each new request + System.out.println("Number of tasks: " + listOfTasks.value.size()); } public Optional retrieveTaskById(Task.TaskId id) { @@ -55,6 +67,43 @@ public class TaskList { return Optional.empty(); } + public Task changeTaskStatusToAssigned(Task.TaskId id, Optional serviceProvider) + throws TaskNotFoundException { + return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty()); + } + + public Task changeTaskStatusToRunning(Task.TaskId id, Optional serviceProvider) + throws TaskNotFoundException { + return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty()); + } + + public Task changeTaskStatusToExecuted(Task.TaskId id, Optional serviceProvider, + Optional outputData) throws TaskNotFoundException { + return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData); + } + + private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional serviceProvider, + Optional outputData) { + Optional taskOpt = retrieveTaskById(id); + + if (taskOpt.isEmpty()) { + throw new TaskNotFoundException(); + } + + Task task = taskOpt.get(); + task.setTaskStatus(status); + + if (serviceProvider.isPresent()) { + task.setProvider(serviceProvider.get()); + } + + if (outputData.isPresent()) { + task.setOutputData(outputData.get()); + } + + return task; + } + @Value public static class TaskListName { private String value; @@ -64,5 +113,4 @@ public class TaskList { public static class ListOfTasks { private List value; } - } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java new file mode 100644 index 0000000..830b934 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java @@ -0,0 +1,3 @@ +package ch.unisg.tapastasks.tasks.domain; + +public class TaskNotFoundException extends RuntimeException { } diff --git a/tapas-tasks/src/main/resources/application.properties b/tapas-tasks/src/main/resources/application.properties index 4d360de..fe25873 100644 --- a/tapas-tasks/src/main/resources/application.properties +++ b/tapas-tasks/src/main/resources/application.properties @@ -1 +1,2 @@ server.port=8081 +baseuri=https://tapas-tasks.86-119-34-23.nip.io/ From 52ff8e5cc042331bbac38833a492bb2f823db703 Mon Sep 17 00:00:00 2001 From: Andrei Ciortea Date: Mon, 18 Oct 2021 01:36:28 +0200 Subject: [PATCH 02/40] Update docker-compose.yml --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index dad6b78..b4c85e2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -41,7 +41,7 @@ services: - "traefik.http.routers.tapas-tasks.entryPoints=web,websecure" - "traefik.http.routers.tapas-tasks.tls.certresolver=le" - auction-house: + tapas-auction-house: image: openjdk command: "java -jar /data/tapas-auction-house-0.0.1-SNAPSHOT.jar" restart: unless-stopped From 061a76288de891a3432555d558f655cf229018bf Mon Sep 17 00:00:00 2001 From: Andrei Ciortea Date: Mon, 18 Oct 2021 09:45:52 +0200 Subject: [PATCH 03/40] Update project template for Auction House --- .../http/ExecutorAddedEventListenerHttpAdapter.java | 7 +++---- .../mqtt/ExecutorAddedEventListenerMqttAdapter.java | 6 ++++-- .../adapter/in/web/RetrieveOpenAuctionsWebController.java | 4 ---- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java index 999c61c..3511b7d 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java @@ -11,10 +11,9 @@ 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 -*/ - +/** + * Template for receiving an executor added event via HTTP + */ @RestController public class ExecutorAddedEventListenerHttpAdapter { diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java index 87413f0..2f661d1 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java @@ -7,6 +7,8 @@ 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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.MqttMessage; /** @@ -15,6 +17,7 @@ import org.eclipse.paho.client.mqttv3.MqttMessage; * This class is only provided as an example to help you bootstrap the project. */ public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListener { + private static final Logger LOGGER = LogManager.getLogger(ExecutorAddedEventListenerMqttAdapter.class); @Override public boolean handleEvent(MqttMessage message) { @@ -36,8 +39,7 @@ public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListe ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler(); newExecutorHandler.handleNewExecutorEvent(executorAddedEvent); } catch (JsonProcessingException | NullPointerException e) { - // TODO: refactor logging - e.printStackTrace(); + LOGGER.error(e.getMessage(), e); return false; } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java index bcbf38c..c96a919 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java @@ -53,10 +53,6 @@ public class RetrieveOpenAuctionsWebController { HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json"); - // TODO before providing to students: remove hub links - responseHeaders.add(HttpHeaders.LINK, "; rel=\"hub\""); - responseHeaders.add(HttpHeaders.LINK, "; rel=\"self\""); - return new ResponseEntity<>(array.toString(), responseHeaders, HttpStatus.OK); } } From d08a6d0b673e2aea50d338e10ae7cb3b72d39726 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 18 Oct 2021 11:45:52 +0200 Subject: [PATCH 04/40] merge everything from main project --- .deployment/docker-compose.yml | 15 + .github/workflows/build-and-deploy.yml | 4 + tapas-auction-house/.editorconfig | 9 + tapas-auction-house/.gitignore | 33 ++ .../.mvn/wrapper/MavenWrapperDownloader.java | 117 +++++++ .../.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .../.mvn/wrapper/maven-wrapper.properties | 2 + tapas-auction-house/README.md | 101 ++++++ tapas-auction-house/mvnw | 310 ++++++++++++++++++ tapas-auction-house/mvnw.cmd | 182 ++++++++++ tapas-auction-house/pom.xml | 80 +++++ .../tapas/TapasAuctionHouseApplication.java | 71 ++++ .../common/clients/TapasMqttClient.java | 94 ++++++ .../common/clients/WebSubSubscriber.java | 28 ++ .../formats/AuctionJsonRepresentation.java | 60 ++++ ...ExecutorAddedEventListenerHttpAdapter.java | 34 ++ ...ecutorRemovedEventListenerHttpAdapter.java | 16 + .../mqtt/AuctionEventMqttListener.java | 11 + .../mqtt/AuctionEventsMqttDispatcher.java | 51 +++ ...ExecutorAddedEventListenerMqttAdapter.java | 48 +++ ...tionStartedEventListenerWebSubAdapter.java | 18 + .../in/web/LaunchAuctionWebController.java | 72 ++++ .../RetrieveOpenAuctionsWebController.java | 58 ++++ ...blishAuctionStartedEventWebSubAdapter.java | 37 +++ .../out/web/AuctionWonEventHttpAdapter.java | 20 ++ .../PlaceBidForAuctionCommandHttpAdapter.java | 19 ++ .../handler/AuctionStartedHandler.java | 59 ++++ .../handler/ExecutorAddedHandler.java | 16 + .../handler/ExecutorRemovedHandler.java | 19 ++ .../port/in/AuctionStartedEvent.java | 21 ++ .../port/in/AuctionStartedEventHandler.java | 6 + .../port/in/ExecutorAddedEvent.java | 32 ++ .../port/in/ExecutorAddedEventHandler.java | 6 + .../port/in/ExecutorRemovedEvent.java | 26 ++ .../port/in/ExecutorRemovedEventHandler.java | 6 + .../port/in/LaunchAuctionCommand.java | 37 +++ .../port/in/LaunchAuctionUseCase.java | 8 + .../port/in/RetrieveOpenAuctionsQuery.java | 7 + .../port/in/RetrieveOpenAuctionsUseCase.java | 10 + .../port/out/AuctionStartedEventPort.java | 11 + .../port/out/AuctionWonEventPort.java | 11 + .../port/out/PlaceBidForAuctionCommand.java | 25 ++ .../out/PlaceBidForAuctionCommandPort.java | 6 + .../service/RetrieveOpenAuctionsService.java | 22 ++ .../service/StartAuctionService.java | 113 +++++++ .../tapas/auctionhouse/domain/Auction.java | 171 ++++++++++ .../auctionhouse/domain/AuctionRegistry.java | 105 ++++++ .../domain/AuctionStartedEvent.java | 15 + .../auctionhouse/domain/AuctionWonEvent.java | 16 + .../unisg/tapas/auctionhouse/domain/Bid.java | 66 ++++ .../auctionhouse/domain/ExecutorRegistry.java | 86 +++++ .../common/AuctionHouseResourceDirectory.java | 57 ++++ .../unisg/tapas/common/ConfigProperties.java | 64 ++++ .../ch/unisg/tapas/common/SelfValidating.java | 25 ++ .../src/main/resources/application.properties | 8 + .../TapasAuctionHouseApplicationTests.java | 13 + tapas-tasks/README.md | 152 +++++++-- tapas-tasks/pom.xml | 16 +- .../tapastasks/TapasTasksApplication.java | 7 +- .../formats/TaskJsonPatchRepresentation.java | 102 ++++++ .../in/formats/TaskJsonRepresentation.java | 115 +++++++ .../in/messaging/UnknownEventException.java | 3 + .../TaskAssignedEventListenerHttpAdapter.java | 39 +++ .../http/TaskEventHttpDispatcher.java | 103 ++++++ .../in/messaging/http/TaskEventListener.java | 24 ++ .../TaskExecutedEventListenerHttpAdapter.java | 34 ++ .../TaskStartedEventListenerHttpAdapter.java | 32 ++ .../AddNewTaskToTaskListWebController.java | 58 +++- ...RetrieveTaskFromTaskListWebController.java | 33 +- .../handler/TaskAssignedHandler.java | 19 ++ .../handler/TaskExecutedHandler.java | 19 ++ .../handler/TaskStartedHandler.java | 19 ++ .../port/in/AddNewTaskToTaskListCommand.java | 17 +- .../in/RetrieveTaskFromTaskListUseCase.java | 2 +- .../port/in/TaskAssignedEvent.java | 25 ++ .../port/in/TaskAssignedEventHandler.java | 8 + .../port/in/TaskExecutedEvent.java | 34 ++ .../port/in/TaskExecutedEventHandler.java | 8 + .../application/port/in/TaskStartedEvent.java | 28 ++ .../port/in/TaskStartedEventHandler.java | 8 + .../service/AddNewTaskToTaskListService.java | 8 +- .../RetrieveTaskFromTaskListService.java | 4 +- .../unisg/tapastasks/tasks/domain/Task.java | 68 +++- .../tapastasks/tasks/domain/TaskList.java | 60 +++- .../tasks/domain/TaskNotFoundException.java | 3 + .../src/main/resources/application.properties | 1 + 86 files changed, 3527 insertions(+), 79 deletions(-) create mode 100644 tapas-auction-house/.editorconfig create mode 100644 tapas-auction-house/.gitignore create mode 100644 tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 tapas-auction-house/.mvn/wrapper/maven-wrapper.jar create mode 100644 tapas-auction-house/.mvn/wrapper/maven-wrapper.properties create mode 100644 tapas-auction-house/README.md create mode 100755 tapas-auction-house/mvnw create mode 100644 tapas-auction-house/mvnw.cmd create mode 100644 tapas-auction-house/pom.xml create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java create mode 100644 tapas-auction-house/src/main/resources/application.properties create mode 100644 tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java diff --git a/.deployment/docker-compose.yml b/.deployment/docker-compose.yml index 5aee641..01a5a77 100644 --- a/.deployment/docker-compose.yml +++ b/.deployment/docker-compose.yml @@ -41,6 +41,21 @@ services: - "traefik.http.routers.tapas-tasks.entryPoints=web,websecure" - "traefik.http.routers.tapas-tasks.tls.certresolver=le" + tapas-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-auction-house" + - "traefik.http.services.tapas-auction-house.loadbalancer.server.port=8086" + - "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" + assignment: image: openjdk command: "java -jar /data/assignment-0.0.1-SNAPSHOT.jar" diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 3b82a91..d223887 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -56,6 +56,10 @@ jobs: 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 + - 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 + - run: cp ./.deployment/docker-compose.yml ./target - name: Archive artifacts uses: actions/upload-artifact@v1 diff --git a/tapas-auction-house/.editorconfig b/tapas-auction-house/.editorconfig new file mode 100644 index 0000000..c4f3e5b --- /dev/null +++ b/tapas-auction-house/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true diff --git a/tapas-auction-house/.gitignore b/tapas-auction-house/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/tapas-auction-house/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java b/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..e76d1f3 --- /dev/null +++ b/tapas-auction-house/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/tapas-auction-house/.mvn/wrapper/maven-wrapper.jar b/tapas-auction-house/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties b/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..ffdc10e --- /dev/null +++ b/tapas-auction-house/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/tapas-auction-house/README.md b/tapas-auction-house/README.md new file mode 100644 index 0000000..683500d --- /dev/null +++ b/tapas-auction-house/README.md @@ -0,0 +1,101 @@ +# tapas-auction-house + +The Auction House is the part of your TAPAS application that is largely responsible for the interactions +with the TAPAS applications developed by the other groups. More precisely, it is responsible for +launching and managing auctions and it is implemented following the Hexagonal Architecture (based on +examples from book "Get Your Hands Dirty on Clean Architecture" by Tom Hombergs). + +Technologies: Spring Boot, Maven + +**Note:** this repository contains an [EditorConfig](https://editorconfig.org/) file (`.editorconfig`) +with default editor settings. EditorConfig is supported out-of-the-box by the IntelliJ IDE. To help maintain +consistent code styles, we recommend to reuse this editor configuration file in all your services. + +## Project Overview + +This project provides a partial implementation of the Auction House. The code is documented in detail, +here we only include a summary of implemented features: +* running and managing auctions: + * each auction has a deadline by which it is open for bids + * once the deadline has passed, the auction house closes the auction and selects a random bid +* starting an auction using a command via an HTTP adapter (see sample request below) +* retrieving the list of open auctions via an HTTP adapter, i.e. auctions accepting bids (see sample + request below) +* receiving events when executors are added to the TAPAS application (both via HTTP and MQTT adapters) +* the logic for automatic placement of bids in auctions: the auction house will place a bid in every + auction for which there is at least one executor that can handle the type of task + being auctioned +* discovery of auction houses via a provided resource directory (see assignment sheet for + Exercises 5 & 6 for more details) + +## Overview of Adapters + +In addition to the overall skeleton of the auction house, the current partial implementation provides +several adapters to help you get started. + +### HTTP Adapters + +Sample HTTP request for launching an auction: + +```shell +curl -i --location --request POST 'http://localhost:8083/auctions/' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "taskUri" : "http://example.org", + "taskType" : "taskType1", + "deadline" : 10000 +}' + +HTTP/1.1 201 +Content-Type: application/json +Content-Length: 131 +Date: Sun, 17 Oct 2021 22:34:13 GMT + +{ + "auctionId":"1", + "auctionHouseUri":"http://localhost:8083/", + "taskUri":"http://example.org", + "taskType":"taskType1", + "deadline":10000 +} +``` + +Sample HTTP request for retrieving auctions currently open for bids: + +```shell +curl -i --location --request GET 'http://localhost:8083/auctions/' + +HTTP/1.1 200 +Content-Type: application/json +Content-Length: 133 +Date: Sun, 17 Oct 2021 22:34:20 GMT + +[ + { + "auctionId":"1", + "auctionHouseUri":"http://localhost:8083/", + "taskUri":"http://example.org", + "taskType":"taskType1", + "deadline":10000 + } +] +``` + +Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java) +via an HTTP request: + +```shell +curl -i --location --request POST 'http://localhost:8083/executors/taskType1/executor1' + +HTTP/1.1 204 +Date: Sun, 17 Oct 2021 22:38:45 GMT +``` + +### MQTT Adapters + +Sending an [ExecutorAddedEvent](src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java) +via an MQTT message via HiveMQ's [MQTT CLI](https://hivemq.github.io/mqtt-cli/): + +```shell + mqtt pub -t ch/unisg/tapas-group1/executors -m '{ "taskType" : "taskType1", "executorId" : "executor1" }' +``` diff --git a/tapas-auction-house/mvnw b/tapas-auction-house/mvnw new file mode 100755 index 0000000..a16b543 --- /dev/null +++ b/tapas-auction-house/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/tapas-auction-house/mvnw.cmd b/tapas-auction-house/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/tapas-auction-house/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/tapas-auction-house/pom.xml b/tapas-auction-house/pom.xml new file mode 100644 index 0000000..4b9cbb6 --- /dev/null +++ b/tapas-auction-house/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.3 + + + ch.unisg + tapas-auction-house + 0.0.1-SNAPSHOT + tapas-auction-house + TAPAS Auction House + + 11 + + + + Eclipse Paho Repo + https://repo.eclipse.org/content/repositories/paho-releases/ + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.0 + + + javax.transaction + javax.transaction-api + 1.2 + + + javax.validation + validation-api + 1.1.0.Final + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java new file mode 100644 index 0000000..8fc22d0 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -0,0 +1,71 @@ +package ch.unisg.tapas; + +import ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient; +import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher; +import ch.unisg.tapas.auctionhouse.adapter.common.clients.WebSubSubscriber; +import ch.unisg.tapas.common.AuctionHouseResourceDirectory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.net.URI; +import java.util.List; + +/** + * Main TAPAS Auction House application. + */ +@SpringBootApplication +public class TapasAuctionHouseApplication { + private static final Logger LOGGER = LogManager.getLogger(TapasAuctionHouseApplication.class); + + public static String RESOURCE_DIRECTORY = "https://api.interactions.ics.unisg.ch/auction-houses/"; + public static String MQTT_BROKER = "tcp://broker.hivemq.com:1883"; + + public static void main(String[] args) { + SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class); + + // We will use these bootstrap methods in Week 6: + // bootstrapMarketplaceWithWebSub(); + // bootstrapMarketplaceWithMqtt(); + + tapasAuctioneerApp.run(args); + } + + /** + * Discovers auction houses and subscribes to WebSub notifications + */ + private static void bootstrapMarketplaceWithWebSub() { + List auctionHouseEndpoints = discoverAuctionHouseEndpoints(); + LOGGER.info("Found auction house endpoints: " + auctionHouseEndpoints); + + WebSubSubscriber subscriber = new WebSubSubscriber(); + + for (String endpoint : auctionHouseEndpoints) { + subscriber.subscribeToAuctionHouseEndpoint(URI.create(endpoint)); + } + } + + /** + * Connects to an MQTT broker, presumably the one used by all TAPAS groups to communicate with + * one another + */ + private static void bootstrapMarketplaceWithMqtt() { + try { + AuctionEventsMqttDispatcher dispatcher = new AuctionEventsMqttDispatcher(); + TapasMqttClient client = TapasMqttClient.getInstance(MQTT_BROKER, dispatcher); + client.startReceivingMessages(); + } catch (MqttException e) { + LOGGER.error(e.getMessage(), e); + } + } + + private static List discoverAuctionHouseEndpoints() { + AuctionHouseResourceDirectory rd = new AuctionHouseResourceDirectory( + URI.create(RESOURCE_DIRECTORY) + ); + + return rd.retrieveAuctionHouseEndpoints(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java new file mode 100644 index 0000000..708d512 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java @@ -0,0 +1,94 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.clients; + +import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * MQTT client for your TAPAS application. This class is defined as a singleton, but it does not have + * to be this way. This class is only provided as an example to help you bootstrap your project. + * You are welcomed to change this class as you see fit. + */ +public class TapasMqttClient { + private static final Logger LOGGER = LogManager.getLogger(TapasMqttClient.class); + + private static TapasMqttClient tapasClient = null; + + private MqttClient mqttClient; + private final String mqttClientId; + private final String brokerAddress; + + private final MessageReceivedCallback messageReceivedCallback; + + private final AuctionEventsMqttDispatcher dispatcher; + + private TapasMqttClient(String brokerAddress, AuctionEventsMqttDispatcher dispatcher) { + this.mqttClientId = UUID.randomUUID().toString(); + this.brokerAddress = brokerAddress; + + this.messageReceivedCallback = new MessageReceivedCallback(); + + this.dispatcher = dispatcher; + } + + public static synchronized TapasMqttClient getInstance(String brokerAddress, + AuctionEventsMqttDispatcher dispatcher) { + + if (tapasClient == null) { + tapasClient = new TapasMqttClient(brokerAddress, dispatcher); + } + + return tapasClient; + } + + public void startReceivingMessages() throws MqttException { + mqttClient = new org.eclipse.paho.client.mqttv3.MqttClient(brokerAddress, mqttClientId, new MemoryPersistence()); + mqttClient.connect(); + mqttClient.setCallback(messageReceivedCallback); + + subscribeToAllTopics(); + } + + public void stopReceivingMessages() throws MqttException { + mqttClient.disconnect(); + } + + private void subscribeToAllTopics() throws MqttException { + for (String topic : dispatcher.getAllTopics()) { + subscribeToTopic(topic); + } + } + + private void subscribeToTopic(String topic) throws MqttException { + mqttClient.subscribe(topic); + } + + private void publishMessage(String topic, String payload) throws MqttException { + MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); + mqttClient.publish(topic, message); + } + + private class MessageReceivedCallback implements MqttCallback { + + @Override + public void connectionLost(Throwable cause) { } + + @Override + public void messageArrived(String topic, MqttMessage message) { + LOGGER.info("Received new MQTT message for topic " + topic + ": " + + new String(message.getPayload())); + + if (topic != null && !topic.isEmpty()) { + dispatcher.dispatchEvent(topic, message); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java new file mode 100644 index 0000000..da2b096 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java @@ -0,0 +1,28 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.clients; + +import java.net.URI; + +/** + * Subscribes to the WebSub hubs of auction houses discovered at run time. This class is instantiated + * from {@link ch.unisg.tapas.TapasAuctionHouseApplication} when boostraping the TAPAS marketplace + * via WebSub. + */ +public class WebSubSubscriber { + + public void subscribeToAuctionHouseEndpoint(URI endpoint) { + // TODO Subscribe to the auction house endpoint via WebSub: + // 1. Send a request to the auction house in order to discover the WebSub hub to subscribe to. + // The request URI should depend on the design of the Auction House HTTP API. + // 2. Send a subscription request to the discovered WebSub hub to subscribe to events relevant + // for this auction house. + // 3. Handle the validation of intent from the WebSub hub (see WebSub protocol). + // + // Once the subscription is activated, the hub will send "fat pings" with content updates. + // The content received from the hub will depend primarily on the design of the Auction House + // HTTP API. + // + // For further details see: + // - W3C WebSub Recommendation: https://www.w3.org/TR/websub/ + // - the implementation notes of the WebSub hub you are using to distribute events + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java new file mode 100644 index 0000000..4500423 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java @@ -0,0 +1,60 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.formats; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; + +/** + * Used to expose a representation of the state of an auction through an interface. This class is + * only meant as a starting point when defining a uniform HTTP API for the Auction House: feel free + * to modify this class as you see fit! + */ +public class AuctionJsonRepresentation { + public static final String MEDIA_TYPE = "application/json"; + + @Getter @Setter + private String auctionId; + + @Getter @Setter + private String auctionHouseUri; + + @Getter @Setter + private String taskUri; + + @Getter @Setter + private String taskType; + + @Getter @Setter + private Integer deadline; + + public AuctionJsonRepresentation() { } + + public AuctionJsonRepresentation(String auctionId, String auctionHouseUri, String taskUri, + String taskType, Integer deadline) { + this.auctionId = auctionId; + this.auctionHouseUri = auctionHouseUri; + this.taskUri = taskUri; + this.taskType = taskType; + this.deadline = deadline; + } + + public AuctionJsonRepresentation(Auction auction) { + this.auctionId = auction.getAuctionId().getValue(); + this.auctionHouseUri = auction.getAuctionHouseUri().getValue().toString(); + this.taskUri = auction.getTaskUri().getValue().toString(); + this.taskType = auction.getTaskType().getValue(); + this.deadline = auction.getDeadline().getValue(); + } + + public static String serialize(Auction auction) throws JsonProcessingException { + AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return mapper.writeValueAsString(representation); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java new file mode 100644 index 0000000..3511b7d --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java @@ -0,0 +1,34 @@ +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; + +/** + * Template for receiving an executor added event via HTTP + */ +@RestController +public class ExecutorAddedEventListenerHttpAdapter { + + @PostMapping(path = "/executors/{taskType}/{executorId}") + public ResponseEntity handleExecutorAddedEvent(@PathVariable("taskType") String taskType, + @PathVariable("executorId") String executorId) { + + ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( + new ExecutorRegistry.ExecutorIdentifier(executorId), + new Auction.AuctionedTaskType(taskType) + ); + + ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler(); + newExecutorHandler.handleNewExecutorEvent(executorAddedEvent); + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java new file mode 100644 index 0000000..53811f9 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java @@ -0,0 +1,16 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http; + +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +/** + * Template for handling an executor removed event received via an HTTP request + */ +@RestController +public class ExecutorRemovedEventListenerHttpAdapter { + + // TODO: add annotations for request method, request URI, etc. + public void handleExecutorRemovedEvent(@PathVariable("executorId") String executorId) { + // TODO: implement logic + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java new file mode 100644 index 0000000..6da39e6 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventMqttListener.java @@ -0,0 +1,11 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * Abstract MQTT listener for auction-related events + */ +public abstract class AuctionEventMqttListener { + + public abstract boolean handleEvent(MqttMessage message); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java new file mode 100644 index 0000000..e5eaf12 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java @@ -0,0 +1,51 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import org.eclipse.paho.client.mqttv3.*; + +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +/** + * Dispatches MQTT messages for known topics to associated event listeners. Used in conjunction with + * {@link ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient}. + * + * This is where you would define MQTT topics and map them to event listeners (see + * {@link AuctionEventsMqttDispatcher#initRouter()}). + * + * This class is only provided as an example to help you bootstrap the project. You are welcomed to + * change this class as you see fit. + */ +public class AuctionEventsMqttDispatcher { + private final Map router; + + public AuctionEventsMqttDispatcher() { + this.router = new Hashtable<>(); + initRouter(); + } + + // TODO: Register here your topics and event listener adapters + private void initRouter() { + router.put("ch/unisg/tapas-group-tutors/executors", new ExecutorAddedEventListenerMqttAdapter()); + } + + /** + * Returns all topics registered with this dispatcher. + * + * @return the set of registered topics + */ + public Set getAllTopics() { + return router.keySet(); + } + + /** + * Dispatches an event received via MQTT for a given topic. + * + * @param topic the topic for which the MQTT message was received + * @param message the received MQTT message + */ + public void dispatchEvent(String topic, MqttMessage message) { + AuctionEventMqttListener listener = router.get(topic); + listener.handleEvent(message); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java new file mode 100644 index 0000000..2f661d1 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java @@ -0,0 +1,48 @@ +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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +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 { + private static final Logger LOGGER = LogManager.getLogger(ExecutorAddedEventListenerMqttAdapter.class); + + @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) { + LOGGER.error(e.getMessage(), e); + return false; + } + + return true; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java new file mode 100644 index 0000000..d156452 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java @@ -0,0 +1,18 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub; + +import ch.unisg.tapas.auctionhouse.application.handler.AuctionStartedHandler; +import org.springframework.web.bind.annotation.*; + +/** + * This class is a template for handling auction started events received via WebSub + */ +@RestController +public class AuctionStartedEventListenerWebSubAdapter { + private final AuctionStartedHandler auctionStartedHandler; + + public AuctionStartedEventListenerWebSubAdapter(AuctionStartedHandler auctionStartedHandler) { + this.auctionStartedHandler = auctionStartedHandler; + } + + //TODO +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java new file mode 100644 index 0000000..c65631e --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/LaunchAuctionWebController.java @@ -0,0 +1,72 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.web; + +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.net.URI; + +/** + * Controller that handles HTTP requests for launching auctions. This controller implements the + * {@link LaunchAuctionUseCase} use case using the {@link LaunchAuctionCommand}. + */ +@RestController +public class LaunchAuctionWebController { + private final LaunchAuctionUseCase launchAuctionUseCase; + + /** + * Constructs the controller. + * + * @param launchAuctionUseCase an implementation of the launch auction use case + */ + public LaunchAuctionWebController(LaunchAuctionUseCase launchAuctionUseCase) { + this.launchAuctionUseCase = launchAuctionUseCase; + } + + /** + * Handles HTTP POST requests for launching auctions. Note: you are free to modify this handler + * as you see fit to reflect the discussions for the uniform HTTP API for the auction house. + * You should also ensure that this handler has the exact behavior you would expect from the + * defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.) + * + * @param payload a representation of the auction to be launched + * @return + */ + @PostMapping(path = "/auctions/", consumes = AuctionJsonRepresentation.MEDIA_TYPE) + public ResponseEntity launchAuction(@RequestBody AuctionJsonRepresentation payload) { + Auction.AuctionDeadline deadline = (payload.getDeadline() == null) ? + null : new Auction.AuctionDeadline(payload.getDeadline()); + + LaunchAuctionCommand command = new LaunchAuctionCommand( + new Auction.AuctionedTaskUri(URI.create(payload.getTaskUri())), + new Auction.AuctionedTaskType(payload.getTaskType()), + deadline + ); + + // This command returns the created auction. We need the created auction to be able to + // include a representation of it in the HTTP response. + Auction auction = launchAuctionUseCase.launchAuction(command); + + try { + AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction); + String auctionJson = AuctionJsonRepresentation.serialize(auction); + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, AuctionJsonRepresentation.MEDIA_TYPE); + + // Return a 201 Created status code and a representation of the created auction + return new ResponseEntity<>(auctionJson, responseHeaders, HttpStatus.CREATED); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java new file mode 100644 index 0000000..c96a919 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/RetrieveOpenAuctionsWebController.java @@ -0,0 +1,58 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.web; + +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery; +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collection; + +/** + * Controller that handles HTTP requests for retrieving auctions hosted by this auction house that + * are open for bids. This controller implements the {@link RetrieveOpenAuctionsUseCase} use case + * using the {@link RetrieveOpenAuctionsQuery}. + */ +@RestController +public class RetrieveOpenAuctionsWebController { + private final RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase; + + public RetrieveOpenAuctionsWebController(RetrieveOpenAuctionsUseCase retrieveAuctionListUseCase) { + this.retrieveAuctionListUseCase = retrieveAuctionListUseCase; + } + + /** + * Handles HTTP GET requests to retrieve the auctions that are open. Note: you are free to modify + * this handler as you see fit to reflect the discussions for the uniform HTTP API for the + * auction house. You should also ensure that this handler has the exact behavior you would expect + * from the defined uniform HTTP API (status codes, returned payload, HTTP headers, etc.). + * + * @return a representation of a collection with the auctions that are open for bids + */ + @GetMapping(path = "/auctions/") + public ResponseEntity retrieveOpenAuctions() { + Collection auctions = + retrieveAuctionListUseCase.retrieveAuctions(new RetrieveOpenAuctionsQuery()); + + ObjectMapper mapper = new ObjectMapper(); + ArrayNode array = mapper.createArrayNode(); + + for (Auction auction : auctions) { + AuctionJsonRepresentation representation = new AuctionJsonRepresentation(auction); + JsonNode node = mapper.valueToTree(representation); + array.add(node); + } + + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json"); + + return new ResponseEntity<>(array.toString(), responseHeaders, HttpStatus.OK); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java new file mode 100644 index 0000000..9e6ec67 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java @@ -0,0 +1,37 @@ +package ch.unisg.tapas.auctionhouse.adapter.out.messaging.websub; + +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent; +import ch.unisg.tapas.common.ConfigProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * This class is a template for publishing auction started events via WebSub. + */ +@Component +@Primary +public class PublishAuctionStartedEventWebSubAdapter implements AuctionStartedEventPort { + // You can use this object to retrieve properties from application.properties, e.g. the + // WebSub hub publish endpoint, etc. + @Autowired + private ConfigProperties config; + + @Override + public void publishAuctionStartedEvent(AuctionStartedEvent event) { + // TODO + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java new file mode 100644 index 0000000..26949f2 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java @@ -0,0 +1,20 @@ +package ch.unisg.tapas.auctionhouse.adapter.out.web; + +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort; +import ch.unisg.tapas.auctionhouse.domain.AuctionWonEvent; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +/** + * This class is a template for sending auction won events via HTTP. This class was created here only + * as a placeholder, it is up to you to decide how such events should be sent (e.g., via HTTP, + * WebSub, etc.). + */ +@Component +@Primary +public class AuctionWonEventHttpAdapter implements AuctionWonEventPort { + @Override + public void publishAuctionWonEvent(AuctionWonEvent event) { + + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java new file mode 100644 index 0000000..6db8c68 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/PlaceBidForAuctionCommandHttpAdapter.java @@ -0,0 +1,19 @@ +package ch.unisg.tapas.auctionhouse.adapter.out.web; + +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommandPort; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +/** + * This class is a tempalte for implementing a place bid for auction command via HTTP. + */ +@Component +@Primary +public class PlaceBidForAuctionCommandHttpAdapter implements PlaceBidForAuctionCommandPort { + + @Override + public void placeBid(PlaceBidForAuctionCommand command) { + // TODO + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java new file mode 100644 index 0000000..e4b312f --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/AuctionStartedHandler.java @@ -0,0 +1,59 @@ +package ch.unisg.tapas.auctionhouse.application.handler; + +import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEvent; +import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEventHandler; +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.out.PlaceBidForAuctionCommandPort; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Bid; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import ch.unisg.tapas.common.ConfigProperties; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Handler for auction started events. This handler will automatically bid in any auction for a + * task of known type, i.e. a task for which the auction house knows an executor is available. + */ +@Component +public class AuctionStartedHandler implements AuctionStartedEventHandler { + private static final Logger LOGGER = LogManager.getLogger(AuctionStartedHandler.class); + + @Autowired + private ConfigProperties config; + + @Autowired + private PlaceBidForAuctionCommandPort placeBidForAuctionCommandPort; + + /** + * Handles an auction started event and bids in all auctions for tasks of known types. + * + * @param auctionStartedEvent the auction started domain event + * @return true unless a runtime exception occurs + */ + @Override + public boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent) { + Auction auction = auctionStartedEvent.getAuction(); + + if (ExecutorRegistry.getInstance().containsTaskType(auction.getTaskType())) { + LOGGER.info("Placing bid for task " + auction.getTaskUri() + " of type " + + auction.getTaskType() + " in auction " + auction.getAuctionId() + + " from auction house " + auction.getAuctionHouseUri().getValue().toString()); + + Bid bid = new Bid(auction.getAuctionId(), + new Bid.BidderName(config.getGroupName()), + new Bid.BidderAuctionHouseUri(config.getAuctionHouseUri()), + new Bid.BidderTaskListUri(config.getTaskListUri()) + ); + + PlaceBidForAuctionCommand command = new PlaceBidForAuctionCommand(auction, bid); + placeBidForAuctionCommandPort.placeBid(command); + } else { + LOGGER.info("Cannot execute this task type: " + auction.getTaskType().getValue()); + } + + return true; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java new file mode 100644 index 0000000..624e669 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java @@ -0,0 +1,16 @@ +package ch.unisg.tapas.auctionhouse.application.handler; + +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEvent; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorAddedEventHandler; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import org.springframework.stereotype.Component; + +@Component +public class ExecutorAddedHandler implements ExecutorAddedEventHandler { + + @Override + public boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent) { + return ExecutorRegistry.getInstance().addExecutor(executorAddedEvent.getTaskType(), + executorAddedEvent.getExecutorId()); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java new file mode 100644 index 0000000..c3bfed8 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapas.auctionhouse.application.handler; + +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEvent; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEventHandler; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import org.springframework.stereotype.Component; + +/** + * Handler for executor removed events. It removes the executor from this auction house's executor + * registry. + */ +@Component +public class ExecutorRemovedHandler implements ExecutorRemovedEventHandler { + + @Override + public boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent) { + return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorId()); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java new file mode 100644 index 0000000..b937e26 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEvent.java @@ -0,0 +1,21 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Event that notifies this auction house that an auction was started by another auction house. + */ +@Value +public class AuctionStartedEvent extends SelfValidating { + @NotNull + private final Auction auction; + + public AuctionStartedEvent(Auction auction) { + this.auction = auction; + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java new file mode 100644 index 0000000..1eed1d9 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/AuctionStartedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +public interface AuctionStartedEventHandler { + + boolean handleAuctionStartedEvent(AuctionStartedEvent auctionStartedEvent); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java new file mode 100644 index 0000000..5a53b94 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java @@ -0,0 +1,32 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskType; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Event that notifies the auction house that an executor has been added to this TAPAS application. + */ +@Value +public class ExecutorAddedEvent extends SelfValidating { + @NotNull + private final ExecutorIdentifier executorId; + + @NotNull + private final AuctionedTaskType taskType; + + /** + * Constructs an executor added event. + * + * @param executorId the identifier of the executor that was added to this TAPAS application + */ + public ExecutorAddedEvent(ExecutorIdentifier executorId, AuctionedTaskType taskType) { + this.executorId = executorId; + this.taskType = taskType; + + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java new file mode 100644 index 0000000..ca82a1c --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +public interface ExecutorAddedEventHandler { + + boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java new file mode 100644 index 0000000..4d5c910 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java @@ -0,0 +1,26 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Event that notifies the auction house that an executor has been removed from this TAPAS application. + */ +@Value +public class ExecutorRemovedEvent extends SelfValidating { + @NotNull + private final ExecutorIdentifier executorId; + + /** + * Constructs an executor removed event. + * + * @param executorId the identifier of the executor that was removed from this TAPAS application + */ + public ExecutorRemovedEvent(ExecutorIdentifier executorId) { + this.executorId = executorId; + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java new file mode 100644 index 0000000..6d92422 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +public interface ExecutorRemovedEventHandler { + + boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java new file mode 100644 index 0000000..626fa49 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java @@ -0,0 +1,37 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Command for launching an auction in this auction house. + */ +@Value +public class LaunchAuctionCommand extends SelfValidating { + @NotNull + private final Auction.AuctionedTaskUri taskUri; + + @NotNull + private final Auction.AuctionedTaskType taskType; + + private final Auction.AuctionDeadline deadline; + + /** + * Constructs the launch action command. + * + * @param taskUri the URI of the auctioned task + * @param taskType the type of the auctioned task + * @param deadline the deadline by which the auction should receive bids (can be null if none) + */ + public LaunchAuctionCommand(Auction.AuctionedTaskUri taskUri, Auction.AuctionedTaskType taskType, + Auction.AuctionDeadline deadline) { + this.taskUri = taskUri; + this.taskType = taskType; + this.deadline = deadline; + + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java new file mode 100644 index 0000000..261240b --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionUseCase.java @@ -0,0 +1,8 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; + +public interface LaunchAuctionUseCase { + + Auction launchAuction(LaunchAuctionCommand command); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java new file mode 100644 index 0000000..a77f267 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsQuery.java @@ -0,0 +1,7 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +/** + * Query used to retrieve open auctions. Although this query is empty, we model it to convey the + * domain semantics and to reduce coupling. + */ +public class RetrieveOpenAuctionsQuery { } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java new file mode 100644 index 0000000..4f94df2 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/RetrieveOpenAuctionsUseCase.java @@ -0,0 +1,10 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Auction; + +import java.util.Collection; + +public interface RetrieveOpenAuctionsUseCase { + + Collection retrieveAuctions(RetrieveOpenAuctionsQuery query); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java new file mode 100644 index 0000000..9a432c9 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionStartedEventPort.java @@ -0,0 +1,11 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent; + +/** + * Port for sending out auction started events + */ +public interface AuctionStartedEventPort { + + void publishAuctionStartedEvent(AuctionStartedEvent event); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java new file mode 100644 index 0000000..7ed440f --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/AuctionWonEventPort.java @@ -0,0 +1,11 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +import ch.unisg.tapas.auctionhouse.domain.AuctionWonEvent; + +/** + * Port for sending out auction won events + */ +public interface AuctionWonEventPort { + + void publishAuctionWonEvent(AuctionWonEvent event); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java new file mode 100644 index 0000000..e207891 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommand.java @@ -0,0 +1,25 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Bid; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +/** + * Command to place a bid for a given auction. + */ +@Value +public class PlaceBidForAuctionCommand extends SelfValidating { + @NotNull + private final Auction auction; + @NotNull + private final Bid bid; + + public PlaceBidForAuctionCommand(Auction auction, Bid bid) { + this.auction = auction; + this.bid = bid; + this.validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java new file mode 100644 index 0000000..3bf5a16 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/out/PlaceBidForAuctionCommandPort.java @@ -0,0 +1,6 @@ +package ch.unisg.tapas.auctionhouse.application.port.out; + +public interface PlaceBidForAuctionCommandPort { + + void placeBid(PlaceBidForAuctionCommand command); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java new file mode 100644 index 0000000..bdba393 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/RetrieveOpenAuctionsService.java @@ -0,0 +1,22 @@ +package ch.unisg.tapas.auctionhouse.application.service; + +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsQuery; +import ch.unisg.tapas.auctionhouse.application.port.in.RetrieveOpenAuctionsUseCase; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.AuctionRegistry; +import org.springframework.stereotype.Component; + +import java.util.Collection; + +/** + * Service that implements {@link RetrieveOpenAuctionsUseCase} to retrieve all auctions in this auction + * house that are open for bids. + */ +@Component +public class RetrieveOpenAuctionsService implements RetrieveOpenAuctionsUseCase { + + @Override + public Collection retrieveAuctions(RetrieveOpenAuctionsQuery query) { + return AuctionRegistry.getInstance().getOpenAuctions(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java new file mode 100644 index 0000000..42c6e37 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java @@ -0,0 +1,113 @@ +package ch.unisg.tapas.auctionhouse.application.service; + +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionCommand; +import ch.unisg.tapas.auctionhouse.application.port.in.LaunchAuctionUseCase; +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort; +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort; +import ch.unisg.tapas.auctionhouse.domain.*; +import ch.unisg.tapas.common.ConfigProperties; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Service that implements the {@link LaunchAuctionUseCase} to start an auction. If a deadline is + * specified for the auction, the service automatically closes the auction at the deadline. If a + * deadline is not specified, the service closes the auction after 10s by default. + */ +@Component +public class StartAuctionService implements LaunchAuctionUseCase { + private static final Logger LOGGER = LogManager.getLogger(StartAuctionService.class); + + private final static int DEFAULT_AUCTION_DEADLINE_MILLIS = 10000; + + // Event port used to publish an auction started event + private final AuctionStartedEventPort auctionStartedEventPort; + // Event port used to publish an auction won event + private final AuctionWonEventPort auctionWonEventPort; + + private final ScheduledExecutorService service; + private final AuctionRegistry auctions; + + @Autowired + private ConfigProperties config; + + public StartAuctionService(AuctionStartedEventPort auctionStartedEventPort, + AuctionWonEventPort auctionWonEventPort) { + this.auctionStartedEventPort = auctionStartedEventPort; + this.auctionWonEventPort = auctionWonEventPort; + this.auctions = AuctionRegistry.getInstance(); + this.service = Executors.newScheduledThreadPool(1); + } + + /** + * Launches an auction. + * + * @param command the domain command used to launch the auction (see {@link LaunchAuctionCommand}) + * @return the launched auction + */ + @Override + public Auction launchAuction(LaunchAuctionCommand command) { + Auction.AuctionDeadline deadline = (command.getDeadline() == null) ? + new Auction.AuctionDeadline(DEFAULT_AUCTION_DEADLINE_MILLIS) : command.getDeadline(); + + // Create a new auction and add it to the auction registry + Auction auction = new Auction(new Auction.AuctionHouseUri(config.getAuctionHouseUri()), + command.getTaskUri(), command.getTaskType(), deadline); + auctions.addAuction(auction); + + // Schedule the closing of the auction at the deadline + service.schedule(new CloseAuctionTask(auction.getAuctionId()), deadline.getValue(), + TimeUnit.MILLISECONDS); + + // Publish an auction started event + AuctionStartedEvent auctionStartedEvent = new AuctionStartedEvent(auction); + auctionStartedEventPort.publishAuctionStartedEvent(auctionStartedEvent); + + return auction; + } + + /** + * This task closes the auction at the deadline and selects a winner if any bids were placed. It + * also sends out associated events and commands. + */ + private class CloseAuctionTask implements Runnable { + Auction.AuctionId auctionId; + + public CloseAuctionTask(Auction.AuctionId auctionId) { + this.auctionId = auctionId; + } + + @Override + public void run() { + Optional auctionOpt = auctions.getAuctionById(auctionId); + + if (auctionOpt.isPresent()) { + Auction auction = auctionOpt.get(); + Optional bid = auction.selectBid(); + + // Close the auction + auction.close(); + + if (bid.isPresent()) { + // Notify the bidder + Bid.BidderName bidderName = bid.get().getBidderName(); + LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " for task " + + auction.getTaskUri().getValue() + " won by " + bidderName.getValue()); + + // Send an auction won event for the winning bid + auctionWonEventPort.publishAuctionWonEvent(new AuctionWonEvent(bid.get())); + } else { + LOGGER.info("Auction #" + auction.getAuctionId().getValue() + " ended with no bids for task " + + auction.getTaskUri().getValue()); + } + } + } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java new file mode 100644 index 0000000..3e51ef7 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java @@ -0,0 +1,171 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; +import lombok.Value; + +import java.net.URI; +import java.util.*; + +/** + * Domain entity that models an auction. + */ +public class Auction { + // Auctions have two possible states: + // - open: waiting for bids + // - closed: the auction deadline has expired, there may or may not be a winning bid + public enum Status { + OPEN, CLOSED + } + + // One way to generate auction identifiers is incremental starting from 1. This makes identifiers + // predictable, which can help with debugging when multiple parties are interacting, but it also + // means that auction identifiers are not universally unique unless they are part of a URI. + // An alternative would be to use UUIDs (see constructor). + private static long AUCTION_COUNTER = 1; + + @Getter + private AuctionId auctionId; + + @Getter + private AuctionStatus auctionStatus; + + // URI that identifies the auction house that started this auction. Given a uniform, standard + // HTTP API for auction houses, this URI can then be used as a base URI for interacting with + // the identified auction house. + @Getter + private final AuctionHouseUri auctionHouseUri; + + // URI that identifies the task for which the auction was launched. URIs are uniform identifiers + // and can be referenced independent of context: because we have defined a uniform HTTP API for + // TAPAS-Tasks, we can dereference this URI to retrieve a complete representation of the + // auctioned task. + @Getter + private final AuctionedTaskUri taskUri; + + // The type of the task being auctioned. We could also retrieve the task type by dereferencing + // the task's URI, but given that the bidding is defined primarily based on task types, knowing + // the task type avoids an additional HTTP request. + @Getter + private final AuctionedTaskType taskType; + + // The deadline by which bids can be placed. Once the deadline expires, the auction is closed. + @Getter + private final AuctionDeadline deadline; + + // Available bids. + @Getter + private final List bids; + + /** + * Constructs an auction. + * + * @param auctionHouseUri the URI of the auction hause that started the auction + * @param taskUri the URI of the task being auctioned + * @param taskType the type of the task being auctioned + * @param deadline the deadline by which the auction is open for bids + */ + public Auction(AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri, + AuctionedTaskType taskType, AuctionDeadline deadline) { + // Generates an incremental identifier + this.auctionId = new AuctionId("" + AUCTION_COUNTER ++); + // As an alternative, we could also generate an UUID + // this.auctionId = new AuctionId(UUID.randomUUID().toString()); + + this.auctionStatus = new AuctionStatus(Status.OPEN); + + this.auctionHouseUri = auctionHouseUri; + this.taskUri = taskUri; + this.taskType = taskType; + + this.deadline = deadline; + this.bids = new ArrayList<>(); + } + + /** + * Constructs an auction. + * + * @param auctionId the identifier of the auction + * @param auctionHouseUri the URI of the auction hause that started the auction + * @param taskUri the URI of the task being auctioned + * @param taskType the type of the task being auctioned + * @param deadline the deadline by which the auction is open for bids + */ + public Auction(AuctionId auctionId, AuctionHouseUri auctionHouseUri, AuctionedTaskUri taskUri, + AuctionedTaskType taskType, AuctionDeadline deadline) { + this(auctionHouseUri, taskUri, taskType, deadline); + this.auctionId = auctionId; + } + + /** + * Places a bid for this auction. + * + * @param bid the bid + */ + public void addBid(Bid bid) { + bids.add(bid); + } + + /** + * Selects a bid randomly from the bids available for this auction. + * + * @return a winning bid or Optional.empty if no bid was made in this auction. + */ + public Optional selectBid() { + if (bids.isEmpty()) { + return Optional.empty(); + } + + int index = new Random().nextInt(bids.size()); + return Optional.of(bids.get(index)); + } + + /** + * Checks if the auction is open for bids. + * + * @return true if open for bids, false if the auction is closed + */ + public boolean isOpen() { + return auctionStatus.getValue() == Status.OPEN; + } + + /** + * Closes the auction. Called by the StartAuctionService after the auction deadline has expired. + */ + public void close() { + auctionStatus = new AuctionStatus(Status.CLOSED); + } + + /* + * Definitions of Value Objects + */ + + @Value + public static class AuctionId { + String value; + } + + @Value + public static class AuctionStatus { + Status value; + } + + @Value + public static class AuctionHouseUri { + URI value; + } + + @Value + public static class AuctionedTaskUri { + URI value; + } + + @Value + public static class AuctionedTaskType { + String value; + } + + @Value + public static class AuctionDeadline { + int value; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java new file mode 100644 index 0000000..858589d --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionRegistry.java @@ -0,0 +1,105 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import java.util.Collection; +import java.util.Hashtable; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Registry that keeps an in-memory history of auctions (both open for bids and closed). This class + * is a singleton. See also {@link Auction}. + */ +public class AuctionRegistry { + private static AuctionRegistry registry; + + private final Map auctions; + + private AuctionRegistry() { + this.auctions = new Hashtable<>(); + } + + /** + * Retrieves a reference to the auction registry. + * + * @return the auction registry + */ + public static synchronized AuctionRegistry getInstance() { + if (registry == null) { + registry = new AuctionRegistry(); + } + + return registry; + } + + /** + * Adds a new auction to the registry + * @param auction the new auction + */ + public void addAuction(Auction auction) { + auctions.put(auction.getAuctionId(), auction); + } + + /** + * Places a bid. See also {@link Bid}. + * + * @param bid the bid to be placed. + * @return false if the bid is for an auction with an unknown identifier, true otherwise + */ + public boolean placeBid(Bid bid) { + if (!containsAuctionWithId(bid.getAuctionId())) { + return false; + } + + Auction auction = getAuctionById(bid.getAuctionId()).get(); + auction.addBid(bid); + auctions.put(bid.getAuctionId(), auction); + + return true; + } + + /** + * Checks if the registry contains an auction with the given identifier. + * + * @param auctionId the auction's identifier + * @return true if the registry contains an auction with the given identifier, false otherwise + */ + public boolean containsAuctionWithId(Auction.AuctionId auctionId) { + return auctions.containsKey(auctionId); + } + + /** + * Retrieves the auction with the given identifier if it exists. + * + * @param auctionId the auction's identifier + * @return the auction or Optional.empty if the identifier is unknown + */ + public Optional getAuctionById(Auction.AuctionId auctionId) { + if (containsAuctionWithId(auctionId)) { + return Optional.of(auctions.get(auctionId)); + } + + return Optional.empty(); + } + + /** + * Retrieves all auctions in the registry. + * + * @return a collection with all auctions + */ + public Collection getAllAuctions() { + return auctions.values(); + } + + /** + * Retrieves only the auctions that are open for bids. + * + * @return a collection with all open auctions + */ + public Collection getOpenAuctions() { + return getAllAuctions() + .stream() + .filter(auction -> auction.isOpen()) + .collect(Collectors.toList()); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java new file mode 100644 index 0000000..7cac1ec --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionStartedEvent.java @@ -0,0 +1,15 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; + +/** + * A domain event that models an auction has started. + */ +public class AuctionStartedEvent { + @Getter + private Auction auction; + + public AuctionStartedEvent(Auction auction) { + this.auction = auction; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java new file mode 100644 index 0000000..484646c --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/AuctionWonEvent.java @@ -0,0 +1,16 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; + +/** + * A domain event that models an auction was won. + */ +public class AuctionWonEvent { + // The winning bid + @Getter + private Bid winningBid; + + public AuctionWonEvent(Bid winningBid) { + this.winningBid = winningBid; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java new file mode 100644 index 0000000..4f24f30 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Bid.java @@ -0,0 +1,66 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; +import lombok.Value; + +import java.net.URI; + +/** + * Domain entity that models a bid. + */ +public class Bid { + // The identifier of the auction for which the bid is placed + @Getter + private final Auction.AuctionId auctionId; + + // The name of the bidder, i.e. the identifier of the TAPAS group + @Getter + private final BidderName bidderName; + + // URI that identifies the auction house of the bidder. Given a uniform, standard HTTP API for + // auction houses, this URI can then be used as a base URI for interacting with the auction house + // of the bidder. + @Getter + private final BidderAuctionHouseUri bidderAuctionHouseUri; + + // URI that identifies the TAPAS-Tasks task list of the bidder. Given a uniform, standard HTTP API + // for TAPAS-Tasks, this URI can then be used as a base URI for interacting with the list of tasks + // of the bidder, e.g. to delegate a task. + @Getter + private final BidderTaskListUri bidderTaskListUri; + + /** + * Constructs a bid. + * + * @param auctionId the identifier of the auction for which the bid is placed + * @param bidderName the name of the bidder, i.e. the identifier of the TAPAS group + * @param auctionHouseUri the URI of the bidder's auction house + * @param taskListUri the URI fo the bidder's list of tasks + */ + public Bid(Auction.AuctionId auctionId, BidderName bidderName, BidderAuctionHouseUri auctionHouseUri, + BidderTaskListUri taskListUri) { + this.auctionId = auctionId; + this.bidderName = bidderName; + this.bidderAuctionHouseUri = auctionHouseUri; + this.bidderTaskListUri = taskListUri; + } + + /* + * Definitions of Value Objects + */ + + @Value + public static class BidderName { + private String value; + } + + @Value + public static class BidderAuctionHouseUri { + private URI value; + } + + @Value + public static class BidderTaskListUri { + private URI value; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java new file mode 100644 index 0000000..9da3756 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java @@ -0,0 +1,86 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Value; + +import java.util.*; + +/** + * Registry that keeps a track of executors internal to the TAPAS application and the types of tasks + * they can achieve. One executor may correspond to multiple task types. This mapping is used when + * bidding for tasks: the auction house will only bid for tasks for which there is a known executor. + * This class is a singleton. + */ +public class ExecutorRegistry { + private static ExecutorRegistry registry; + + private final Map> executors; + + private ExecutorRegistry() { + this.executors = new Hashtable<>(); + } + + public static synchronized ExecutorRegistry getInstance() { + if (registry == null) { + registry = new ExecutorRegistry(); + } + + return registry; + } + + /** + * Adds an executor to the registry for a given task type. + * + * @param taskType the type of the task + * @param executorIdentifier the identifier of the executor (can be any string) + * @return true unless a runtime exception occurs + */ + public boolean addExecutor(Auction.AuctionedTaskType taskType, ExecutorIdentifier executorIdentifier) { + Set taskTypeExecs = executors.getOrDefault(taskType, + Collections.synchronizedSet(new HashSet<>())); + + taskTypeExecs.add(executorIdentifier); + executors.put(taskType, taskTypeExecs); + + return true; + } + + /** + * Removes an executor from the registry. The executor is disassociated from all known task types. + * + * @param executorIdentifier the identifier of the executor (can be any string) + * @return true unless a runtime exception occurs + */ + public boolean removeExecutor(ExecutorIdentifier executorIdentifier) { + Iterator iterator = executors.keySet().iterator(); + + while (iterator.hasNext()) { + Auction.AuctionedTaskType taskType = iterator.next(); + Set set = executors.get(taskType); + + set.remove(executorIdentifier); + + if (set.isEmpty()) { + iterator.remove(); + } + } + + return true; + } + + /** + * Checks if the registry contains an executor for a given task type. Used during an auction to + * decide if a bid should be placed. + * + * @param taskType the task type being auctioned + * @return + */ + public boolean containsTaskType(Auction.AuctionedTaskType taskType) { + return executors.containsKey(taskType); + } + + // Value Object for the executor identifier + @Value + public static class ExecutorIdentifier { + String value; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java new file mode 100644 index 0000000..c4809ef --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/AuctionHouseResourceDirectory.java @@ -0,0 +1,57 @@ +package ch.unisg.tapas.common; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.ArrayList; +import java.util.List; + +/** + * Class that wraps up the resource directory used to discover auction houses in Week 6. + */ +public class AuctionHouseResourceDirectory { + private final URI rdEndpoint; + + /** + * Constructs a resource directory for auction house given a known URI. + * + * @param rdEndpoint the based endpoint of the resource directory + */ + public AuctionHouseResourceDirectory(URI rdEndpoint) { + this.rdEndpoint = rdEndpoint; + } + + /** + * Retrieves the endpoints of all auctions houses registered with this directory. + * @return + */ + public List retrieveAuctionHouseEndpoints() { + List auctionHouseEndpoints = new ArrayList<>(); + + try { + HttpRequest request = HttpRequest.newBuilder() + .uri(rdEndpoint).GET().build(); + + HttpResponse response = HttpClient.newBuilder().build() + .send(request, HttpResponse.BodyHandlers.ofString()); + + // For simplicity, here we just hard code the current representation used by our + // resource directory for auction houses + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode payload = objectMapper.readTree(response.body()); + + for (JsonNode node : payload) { + auctionHouseEndpoints.add(node.get("endpoint").asText()); + } + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + + return auctionHouseEndpoints; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java new file mode 100644 index 0000000..748afda --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java @@ -0,0 +1,64 @@ +package ch.unisg.tapas.common; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.net.URI; + +/** + * Used to access properties provided via application.properties + */ +@Component +public class ConfigProperties { + @Autowired + private Environment environment; + + /** + * Retrieves the URI of the WebSub hub. In this project, we use a single WebSub hub, but we could + * use multiple. + * + * @return the URI of the WebSub hub + */ + public URI getWebSubHub() { + return URI.create(environment.getProperty("websub.hub")); + } + + /** + * Retrieves the URI used to publish content via WebSub. In this project, we use a single + * WebSub hub, but we could use multiple. This URI is usually different from the WebSub hub URI. + * + * @return URI used to publish content via the WebSub hub + */ + public URI getWebSubPublishEndpoint() { + return URI.create(environment.getProperty("websub.hub.publish")); + } + + /** + * Retrieves the name of the group providing this auction house. + * + * @return the identifier of the group, e.g. tapas-group1 + */ + public String getGroupName() { + return environment.getProperty("group"); + } + + /** + * Retrieves the base URI of this auction house. + * + * @return the base URI of this auction house + */ + public URI getAuctionHouseUri() { + return URI.create(environment.getProperty("auction.house.uri")); + } + + /** + * Retrieves the URI of the TAPAS-Tasks task list of this TAPAS applicatoin. This is used, e.g., + * when placing a bid during the auction (see also {@link ch.unisg.tapas.auctionhouse.domain.Bid}). + * + * @return + */ + public URI getTaskListUri() { + return URI.create(environment.getProperty("tasks.list.uri")); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java new file mode 100644 index 0000000..1b56db4 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/SelfValidating.java @@ -0,0 +1,25 @@ +package ch.unisg.tapas.common; + +import javax.validation.*; +import java.util.Set; + +public class SelfValidating { + + private Validator validator; + + public SelfValidating() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + /** + * Evaluates all Bean Validations on the attributes of this + * instance. + */ + protected void validateSelf() { + Set> violations = validator.validate((T) this); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } +} diff --git a/tapas-auction-house/src/main/resources/application.properties b/tapas-auction-house/src/main/resources/application.properties new file mode 100644 index 0000000..e9c609f --- /dev/null +++ b/tapas-auction-house/src/main/resources/application.properties @@ -0,0 +1,8 @@ +server.port=8086 + +websub.hub=https://websub.appspot.com/ +websub.hub.publish=https://websub.appspot.com/ + +group=tapas-group-tutors +auction.house.uri=https://tapas-auction-house.86-119-34-23.nip.io/ +tasks.list.uri=https://tapas-tasks.86-119-34-23.nip.io/ diff --git a/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java b/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java new file mode 100644 index 0000000..ce414c3 --- /dev/null +++ b/tapas-auction-house/src/test/java/ch/unisg/tapas/TapasAuctionHouseApplicationTests.java @@ -0,0 +1,13 @@ +package ch.unisg.tapas; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TapasAuctionHouseApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/tapas-tasks/README.md b/tapas-tasks/README.md index f083776..90016c3 100644 --- a/tapas-tasks/README.md +++ b/tapas-tasks/README.md @@ -11,61 +11,159 @@ with default editor settings. EditorConfig is supported out-of-the-box by the In consistent code styles, we recommend to reuse this editor configuration file in all your services. ## HTTP API Overview -The code we provide includes a minimalistic HTTP API for (i) creating a new task and (ii) retrieving -the representation of a task. +The code we provide includes a minimalistic uniform HTTP API for (i) creating a new task, (ii) retrieving +a representation of the current state of a task, and (iii) patching the representation of a task, which +is mapped to a domain/integration event. + +The representations exchanged with the API use two media types: +* a JSON-based format for task with the media type `application/task+json`; this media type is defined + in the context of our project, but could be [registered with IANA](https://www.iana.org/assignments/media-types) + to promote interoperability (see + [TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java) + for more details) +* the [JSON Patch](http://jsonpatch.com/) format with the registered media type `application/json-patch+json`, which is also a + JSON-based format (see sample HTTP requests below). For further developing and working with your HTTP API, we recommend to use [Postman](https://www.postman.com/). ### Creating a new task A new task is created via an `HTTP POST` request to the `/tasks/` endpoint. The body of the request -must include a JSON payload with the content type `application/json` and two required fields: +must include a representation of the task to be created using the content type `application/task+json` +defined in the context of this project. A valid representation must include at least two required fields +(see [TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java) +for more details): * `taskName`: a string that represents the name of the task to be created * `taskType`: a string that represents the type of the task to be created A sample HTTP request with `curl`: ```shell -curl -i --location --request POST 'http://localhost:8081/tasks/' --header 'Content-Type: application/json' --data-raw '{ - "taskName" : "task1", - "taskType" : "type1" +curl -i --location --request POST 'http://localhost:8081/tasks/' \ +--header 'Content-Type: application/task+json' \ +--data-raw '{ + "taskName" : "task1", + "taskType" : "computation", + "originalTaskUri" : "http://example.org", + "inputData" : "1+1" }' HTTP/1.1 201 -Content-Type: application/json -Content-Length: 142 -Date: Sun, 03 Oct 2021 17:25:32 GMT +Location: http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354 +Content-Type: application/task+json +Content-Length: 170 +Date: Sun, 17 Oct 2021 21:03:34 GMT { - "taskType" : "type1", - "taskState" : "OPEN", - "taskListName" : "tapas-tasks-tutors", - "taskName" : "task1", - "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"OPEN", + "originalTaskUri":"http://example.org", + "inputData":"1+1" } ``` -If the task is created successfuly, a `201 Created` status code is returned together with a JSON -representation of the created task. The representation includes, among others, a _universally unique -identifier (UUID)_ for the newly created task (`taskId`). +If the task is created successfuly, a `201 Created` status code is returned together with a +representation of the created task. The response also includes a `Location` header filed that points +to the URI of the newly created task. ### Retrieving a task -The representation of a task is retrieved via an `HTTP GET` request to the `/tasks/` endpoint. +The representation of a task is retrieved via an `HTTP GET` request to the URI of task. A sample HTTP request with `curl`: ```shell -curl -i --location --request GET 'http://localhost:8081/tasks/53cb19d6-2d9b-486f-98c7-c96c93b037f0' +curl -i --location --request GET 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' HTTP/1.1 200 -Content-Type: application/json -Content-Length: 142 -Date: Sun, 03 Oct 2021 17:27:06 GMT +Content-Type: application/task+json +Content-Length: 170 +Date: Sun, 17 Oct 2021 21:07:04 GMT { - "taskType" : "type1", - "taskState" : "OPEN", - "taskListName" : "tapas-tasks-tutors", - "taskName" : "task1", - "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"OPEN", + "originalTaskUri":"http://example.org", + "inputData":"1+1" } ``` + +### Patching a task + +REST emphasizes the generality of interfaces to promote uniform interaction. For instance, we can use +the `HTTP PATCH` method to implement fine-grained updates to the representational state of a task, which +may translate to various domain/integration events. However, to conform to the uniform interface +contraint in REST, any such updates have to rely on standard knowledge — and thus to hide away the +implementation details of our service. + +In addition to the `application/task+json` media type we defined for our uniform HTTP API, a standard +representation format we can use to specify fine-grained updates to the representation of tasks +is [JSON Patch](http://jsonpatch.com/). In what follow, we provide a few examples of `HTTP PATCH` requests. +For further details on the JSON Patch format, see also [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)). + +#### Changing the status of a task from OPEN to ASSIGNED + +Sample HTTP request that assigns the previously created task to group `tapas-group1`: + +```shell +curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \ +--header 'Content-Type: application/json-patch+json' \ +--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "ASSIGNED" }, + {"op" : "add", "path": "/serviceProvider", "value" : "tapas-group1" } ]' + +HTTP/1.1 200 +Content-Type: application/task+json +Content-Length: 207 +Date: Sun, 17 Oct 2021 21:20:58 GMT + +{ + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"ASSIGNED", + "originalTaskUri":"http://example.org", + "serviceProvider":"tapas-group1", + "inputData":"1+1" +} +``` + +In this example, the requested patch includes two JSON Patch operations: +* an operation to `replace` the `taskStatus` already in the task's representation with the value `ASSIGNED` +* an operation to `add` to the task's representation a `serviceProvider` with the value `tapas-group1` + +Internally, this request is mapped to a +[TaskAssignedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java). +The HTTP response returns a `200 OK` status code together with the updated representation of the task. + +#### Changing the status of a task from to EXECUTED + +Sample HTTP request that changes the status of the task to `EXECUTED` and adds an output result: + +```shell +curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \ +--header 'Content-Type: application/json-patch+json' \ +--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "EXECUTED" }, + {"op" : "add", "path": "/outputData", "value" : "2" } ]' + +HTTP/1.1 200 +Content-Type: application/task+json +Content-Length: 224 +Date: Sun, 17 Oct 2021 21:32:25 GMT + +{ + "taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354", + "taskName":"task1", + "taskType":"computation", + "taskStatus":"EXECUTED", + "originalTaskUri":"http://example.org", + "serviceProvider":"tapas-group1", + "inputData":"1+1", + "outputData":"2" +} +``` + +Internally, this request is mapped to a +[TaskExecutedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java). +The HTTP response returns a `200 OK` status code together with the updated representation of the task. diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml index f3989f4..0118cf9 100644 --- a/tapas-tasks/pom.xml +++ b/tapas-tasks/pom.xml @@ -18,6 +18,12 @@ scs-asse-fs21-group1 https://sonarcloud.io + + + Eclipse Paho Repo + https://repo.eclipse.org/content/repositories/paho-releases/ + + org.springframework.boot @@ -51,11 +57,6 @@ 1.1.0.Final - - org.json - json - 20210307 - com.github.java-json-tools json-patch @@ -69,6 +70,11 @@ true + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.0 + diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java index 90d1716..2675391 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java @@ -3,15 +3,12 @@ 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); + SpringApplication tapasTasksApp = new SpringApplication(TapasTasksApplication.class); + tapasTasksApp.run(args); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java new file mode 100644 index 0000000..94c1f47 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonPatchRepresentation.java @@ -0,0 +1,102 @@ +package ch.unisg.tapastasks.tasks.adapter.in.formats; + +import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * This class is used to process JSON Patch operations for tasks: given a + * JSON Patch for updating the representational state of a task, + * this class provides methods for extracting various operations of interest for our domain (e.g., + * changing the status of a task). + */ +public class TaskJsonPatchRepresentation { + public static final String MEDIA_TYPE = "application/json-patch+json"; + + private final JsonNode patch; + + /** + * Constructs the JSON Patch representation. + * + * @param patch a JSON Patch as JsonNode + */ + public TaskJsonPatchRepresentation(JsonNode patch) { + this.patch = patch; + } + + /** + * Extracts the first task status replaced in this patch. + * + * @return the first task status changed in this patch or an empty {@link Optional} if none is + * found + */ + public Optional extractFirstTaskStatusChange() { + Optional status = extractFirst(node -> + isPatchReplaceOperation(node) && hasPath(node, "/taskStatus") + ); + + if (status.isPresent()) { + String taskStatus = status.get().get("value").asText(); + return Optional.of(Task.Status.valueOf(taskStatus)); + } + + return Optional.empty(); + } + + /** + * Extracts the first service provider added or replaced in this patch. + * + * @return the first service provider changed in this patch or an empty {@link Optional} if none + * is found + */ + public Optional extractFirstServiceProviderChange() { + Optional serviceProvider = extractFirst(node -> + (isPatchReplaceOperation(node) || isPatchAddOperation(node)) + && hasPath(node, "/serviceProvider") + ); + + return (serviceProvider.isEmpty()) ? Optional.empty() + : Optional.of(new Task.ServiceProvider(serviceProvider.get().get("value").asText())); + } + + /** + * Extracts the first output data addition in this patch. + * + * @return the output data added in this patch or an empty {@link Optional} if none is found + */ + public Optional extractFirstOutputDataAddition() { + Optional output = extractFirst(node -> + isPatchAddOperation(node) && hasPath(node, "/outputData") + ); + + return (output.isEmpty()) ? Optional.empty() + : Optional.of(new Task.OutputData(output.get().get("value").asText())); + } + + private Optional extractFirst(Predicate predicate) { + Stream stream = StreamSupport.stream(patch.spliterator(), false); + return stream.filter(predicate).findFirst(); + } + + private boolean isPatchAddOperation(JsonNode node) { + return isPatchOperationOfType(node, "add"); + } + + private boolean isPatchReplaceOperation(JsonNode node) { + return isPatchOperationOfType(node, "replace"); + } + + private boolean isPatchOperationOfType(JsonNode node, String operation) { + return node.isObject() && node.get("op") != null + && node.get("op").asText().equalsIgnoreCase(operation); + } + + private boolean hasPath(JsonNode node, String path) { + return node.isObject() && node.get("path") != null + && node.get("path").asText().equalsIgnoreCase(path); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java new file mode 100644 index 0000000..eb89415 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java @@ -0,0 +1,115 @@ +package ch.unisg.tapastasks.tasks.adapter.in.formats; + +import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; + +/** + * This class is used to expose and consume representations of tasks through the HTTP interface. The + * representations conform to the custom JSON-based media type "application/task+json". The media type + * is just an identifier and can be registered with + * IANA to promote interoperability. + */ +final public class TaskJsonRepresentation { + // The media type used for this task representation format + public static final String MEDIA_TYPE = "application/task+json"; + + // A task identifier specific to our implementation (e.g., a UUID). This identifier is then used + // to generate the task's URI. URIs are standard uniform identifiers and use a universal syntax + // that can be referenced (and dereferenced) independent of context. In our uniform HTTP API, + // we identify tasks via URIs and not implementation-specific identifiers. + @Getter @Setter + private String taskId; + + // A string that represents the task's name + @Getter + private final String taskName; + + // A string that identifies the task's type. This string could also be a URI (e.g., defined in some + // Web ontology, as we shall see later in the course), but it's not constrained to be a URI. + // The task's type can be used to assign executors to tasks, to decide what tasks to bid for, etc. + @Getter + private final String taskType; + + // The task's status: OPEN, ASSIGNED, RUNNING, or EXECUTED (see Task.Status) + @Getter @Setter + private String taskStatus; + + // If this task is a delegated task (i.e., a shadow of another task), this URI points to the + // original task. Because URIs are standard and uniform, we can just dereference this URI to + // retrieve a representation of the original task. + @Getter @Setter + private String originalTaskUri; + + // The service provider who executes this task. The service provider is a any string that identifies + // a TAPAS group (e.g., tapas-group1). This identifier could also be a URI (if we have a good reason + // for it), but it's not constrained to be a URI. + @Getter @Setter + private String serviceProvider; + + // A string that provides domain-specific input data for this task. In the context of this project, + // we can parse and interpret the input data based on the task's type. + @Getter @Setter + private String inputData; + + // A string that provides domain-specific output data for this task. In the context of this project, + // we can parse and interpret the output data based on the task's type. + @Getter @Setter + private String outputData; + + /** + * Instantiate a task representation with a task name and type. + * + * @param taskName string that represents the task's name + * @param taskType string that represents the task's type + */ + public TaskJsonRepresentation(String taskName, String taskType) { + this.taskName = taskName; + this.taskType = taskType; + + this.taskStatus = null; + this.originalTaskUri = null; + this.serviceProvider = null; + this.inputData = null; + this.outputData = null; + } + + /** + * Instantiate a task representation from a domain concept. + * + * @param task the task + */ + public TaskJsonRepresentation(Task task) { + this(task.getTaskName().getValue(), task.getTaskType().getValue()); + + this.taskId = task.getTaskId().getValue(); + this.taskStatus = task.getTaskStatus().getValue().name(); + + this.originalTaskUri = (task.getOriginalTaskUri() == null) ? + null : task.getOriginalTaskUri().getValue(); + + this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue(); + this.inputData = (task.getInputData() == null) ? null : task.getInputData().getValue(); + this.outputData = (task.getOutputData() == null) ? null : task.getOutputData().getValue(); + } + + /** + * Convenience method used to serialize a task provided as a domain concept in the format exposed + * through the uniform HTTP API. + * + * @param task the task as defined in the domain + * @return a string serialization using the JSON-based representation format defined for tasks + * @throws JsonProcessingException if a runtime exception occurs during object serialization + */ + public static String serialize(Task task) throws JsonProcessingException { + TaskJsonRepresentation representation = new TaskJsonRepresentation(task); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return mapper.writeValueAsString(representation); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java new file mode 100644 index 0000000..fbeb7b7 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/UnknownEventException.java @@ -0,0 +1,3 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging; + +public class UnknownEventException extends RuntimeException { } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java new file mode 100644 index 0000000..4c26b80 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskAssignedEventListenerHttpAdapter.java @@ -0,0 +1,39 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.application.handler.TaskAssignedHandler; +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.Task.TaskId; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; + +/** + * Listener for task assigned events. A task assigned event corresponds to a JSON Patch that attempts + * to change the task's status to ASSIGNED and may also add/replace a service provider (i.e., to what + * group the task was assigned). This implementation does not impose that a task assigned event + * includes the service provider (i.e., can be null). + * + * See also {@link TaskAssignedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}. + */ +public class TaskAssignedEventListenerHttpAdapter extends TaskEventListener { + + /** + * Handles the task assigned event. + * + * @param taskId the identifier of the task for which an event was received + * @param payload the JSON Patch payload of the HTTP PATCH request received for this task + * @return + */ + public Task handleTaskEvent(String taskId, JsonNode payload) { + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + Optional serviceProvider = representation.extractFirstServiceProviderChange(); + + TaskAssignedEvent taskAssignedEvent = new TaskAssignedEvent(new TaskId(taskId), serviceProvider); + TaskAssignedEventHandler taskAssignedEventHandler = new TaskAssignedHandler(); + + return taskAssignedEventHandler.handleTaskAssigned(taskAssignedEvent); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java new file mode 100644 index 0000000..940d6fa --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventHttpDispatcher.java @@ -0,0 +1,103 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; +import ch.unisg.tapastasks.tasks.adapter.in.messaging.UnknownEventException; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import com.fasterxml.jackson.databind.JsonNode; +import com.github.fge.jsonpatch.JsonPatch; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +import java.io.IOException; +import java.util.Optional; + + +/** + * This REST Controller handles HTTP PATCH requests for updating the representational state of Task + * resources. Each request to update the representational state of a Task resource can correspond to + * at most one domain/integration event. Request payloads use the + * JSON PATCH format and media type. + * + * A JSON Patch can contain multiple operations (e.g., add, remove, replace) for updating various + * parts of a task's representations. One or more JSON Patch operations can represent a domain/integration + * event. Therefore, the events can only be determined by inspecting the requested patch (e.g., a request + * to change a task's status from RUNNING to EXECUTED). This class is responsible to inspect requested + * patches, identify events, and to route them to appropriate listeners. + * + * For more details on JSON Patch, see: http://jsonpatch.com/ + * For some sample HTTP requests, see the README. + */ +@RestController +public class TaskEventHttpDispatcher { + // The standard media type for JSON Patch registered with IANA + // See: https://www.iana.org/assignments/media-types/application/json-patch+json + private final static String JSON_PATCH_MEDIA_TYPE = "application/json-patch+json"; + + /** + * Handles HTTP PATCH requests with a JSON Patch payload. Routes the requests based on the + * the operations requested in the patch. In this implementation, one HTTP Patch request is + * mapped to at most one domain event. + * + * @param taskId the local (i.e., implementation-specific) identifier of the task to the patched; + * this identifier is extracted from the task's URI + * @param payload the reuqested patch for this task + * @return 200 OK and a representation of the task after processing the event; 404 Not Found if + * the request URI does not match any task; 400 Bad Request if the request is invalid + */ + @PatchMapping(path = "/tasks/{taskId}", consumes = {JSON_PATCH_MEDIA_TYPE}) + public ResponseEntity dispatchTaskEvents(@PathVariable("taskId") String taskId, + @RequestBody JsonNode payload) { + try { + // Throw an exception if the JSON Patch format is invalid. This call is only used to + // validate the JSON PATCH syntax. + JsonPatch.fromJson(payload); + + // Check for known events and route the events to appropriate listeners + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + Optional status = representation.extractFirstTaskStatusChange(); + + TaskEventListener listener = null; + + // Route events related to task status changes + if (status.isPresent()) { + switch (status.get()) { + case ASSIGNED: + listener = new TaskAssignedEventListenerHttpAdapter(); + break; + case RUNNING: + listener = new TaskStartedEventListenerHttpAdapter(); + break; + case EXECUTED: + listener = new TaskExecutedEventListenerHttpAdapter(); + break; + } + } + + if (listener == null) { + // The HTTP PATCH request is valid, but the patch does not match any known event + throw new UnknownEventException(); + } + + Task task = listener.handleTaskEvent(taskId, payload); + + // Add the content type as a response header + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); + + return new ResponseEntity<>(TaskJsonRepresentation.serialize(task), responseHeaders, + HttpStatus.OK); + } catch (TaskNotFoundException e) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND); + } catch (IOException | RuntimeException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java new file mode 100644 index 0000000..8912968 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskEventListener.java @@ -0,0 +1,24 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import com.fasterxml.jackson.databind.JsonNode; + +/** + * Abstract class that handles events specific to a Task. Events are received via an HTTP PATCH + * request for a given task and dispatched to Task event listeners (see {@link TaskEventHttpDispatcher}). + * Each listener must implement the abstract method {@link #handleTaskEvent(String, JsonNode)}, which + * may require additional event-specific validations. + */ +public abstract class TaskEventListener { + + /** + * This abstract method handles a task event and returns the task after the event was handled. + * + * @param taskId the identifier of the task for which an event was received + * @param payload the JSON Patch payload of the HTTP PATCH request received for this task + * @return the task for which the HTTP PATCH request is handled + * @throws TaskNotFoundException + */ + public abstract Task handleTaskEvent(String taskId, JsonNode payload) throws TaskNotFoundException; +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java new file mode 100644 index 0000000..f1db541 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskExecutedEventListenerHttpAdapter.java @@ -0,0 +1,34 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.application.handler.TaskExecutedHandler; +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; + +/** + * Listener for task executed events. A task executed event corresponds to a JSON Patch that attempts + * to change the task's status to EXECUTED, may add/replace a service provider, and may also add an + * output result. This implementation does not impose that a task executed event includes either the + * service provider or an output result (i.e., both can be null). + * + * See also {@link TaskExecutedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}. + */ +public class TaskExecutedEventListenerHttpAdapter extends TaskEventListener { + + public Task handleTaskEvent(String taskId, JsonNode payload) { + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + + Optional serviceProvider = representation.extractFirstServiceProviderChange(); + Optional outputData = representation.extractFirstOutputDataAddition(); + + TaskExecutedEvent taskExecutedEvent = new TaskExecutedEvent(new Task.TaskId(taskId), + serviceProvider, outputData); + TaskExecutedEventHandler taskExecutedEventHandler = new TaskExecutedHandler(); + + return taskExecutedEventHandler.handleTaskExecuted(taskExecutedEvent); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java new file mode 100644 index 0000000..aa2f6b4 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/messaging/http/TaskStartedEventListenerHttpAdapter.java @@ -0,0 +1,32 @@ +package ch.unisg.tapastasks.tasks.adapter.in.messaging.http; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.application.handler.TaskStartedHandler; +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.Task.TaskId; +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Optional; + +/** + * Listener for task started events. A task started event corresponds to a JSON Patch that attempts + * to change the task's status to RUNNING and may also add/replace a service provider. This + * implementation does not impose that a task started event includes the service provider (i.e., + * can be null). + * + * See also {@link TaskStartedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}. + */ +public class TaskStartedEventListenerHttpAdapter extends TaskEventListener { + + public Task handleTaskEvent(String taskId, JsonNode payload) { + TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload); + Optional serviceProvider = representation.extractFirstServiceProviderChange(); + + TaskStartedEvent taskStartedEvent = new TaskStartedEvent(new TaskId(taskId), serviceProvider); + TaskStartedEventHandler taskStartedEventHandler = new TaskStartedHandler(); + + return taskStartedEventHandler.handleTaskStarted(taskStartedEvent); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java index 53bebc1..234dcde 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java @@ -1,8 +1,12 @@ package ch.unisg.tapastasks.tasks.adapter.in.web; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand; import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase; import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -12,29 +16,67 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import javax.validation.ConstraintViolationException; +import java.util.Optional; +/** + * Controller that handles HTTP requests for creating new tasks. This controller implements the + * {@link AddNewTaskToTaskListUseCase} use case using the {@link AddNewTaskToTaskListCommand}. + * + * A new task is created via an HTTP POST request to the /tasks/ endpoint. The body of the request + * contains a JSON-based representation with the "application/task+json" media type defined for this + * project. This custom media type allows to capture the semantics of our JSON representations for + * tasks. + * + * If the request is successful, the controller returns an HTTP 201 Created status code and a + * representation of the created task with Content-Type "application/task+json". The HTTP response + * also include a Location header field that points to the URI of the created task. + */ @RestController public class AddNewTaskToTaskListWebController { private final AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase; + // Used to retrieve properties from application.properties + @Autowired + private Environment environment; + public AddNewTaskToTaskListWebController(AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase) { this.addNewTaskToTaskListUseCase = addNewTaskToTaskListUseCase; } - @PostMapping(path = "/tasks/", consumes = {TaskMediaType.TASK_MEDIA_TYPE}) - public ResponseEntity addNewTaskTaskToTaskList(@RequestBody Task task) { + @PostMapping(path = "/tasks/", consumes = {TaskJsonRepresentation.MEDIA_TYPE}) + public ResponseEntity addNewTaskTaskToTaskList(@RequestBody TaskJsonRepresentation payload) { try { - AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand( - task.getTaskName(), task.getTaskType() - ); + Task.TaskName taskName = new Task.TaskName(payload.getTaskName()); + Task.TaskType taskType = new Task.TaskType(payload.getTaskType()); - Task newTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command); + // If the created task is a delegated task, the representation contains a URI reference + // to the original task + Optional originalTaskUriOptional = + (payload.getOriginalTaskUri() == null) ? Optional.empty() + : Optional.of(new Task.OriginalTaskUri(payload.getOriginalTaskUri())); + + AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(taskName, taskType, + originalTaskUriOptional); + + Task createdTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command); + + // When creating a task, the task's representation may include optional input data + if (payload.getInputData() != null) { + createdTask.setInputData(new Task.InputData(payload.getInputData())); + } // Add the content type as a response header HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); + // Construct and advertise the URI of the newly created task; we retrieve the base URI + // from the application.properties file + responseHeaders.add(HttpHeaders.LOCATION, environment.getProperty("baseuri") + + "tasks/" + createdTask.getTaskId().getValue()); - return new ResponseEntity<>(TaskMediaType.serialize(newTask), responseHeaders, HttpStatus.CREATED); + return new ResponseEntity<>(TaskJsonRepresentation.serialize(createdTask), responseHeaders, + HttpStatus.CREATED); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); } catch (ConstraintViolationException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java index 0eb6bea..d60e4d1 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java @@ -1,8 +1,10 @@ package ch.unisg.tapastasks.tasks.adapter.in.web; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListQuery; import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase; import ch.unisg.tapastasks.tasks.domain.Task; +import com.fasterxml.jackson.core.JsonProcessingException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -11,6 +13,11 @@ import org.springframework.web.server.ResponseStatusException; import java.util.Optional; +/** + * Controller that handles HTTP GET requests for retrieving tasks. This controller implements the + * {@link RetrieveTaskFromTaskListUseCase} use case using the {@link RetrieveTaskFromTaskListQuery} + * query. + */ @RestController public class RetrieveTaskFromTaskListWebController { private final RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase; @@ -19,10 +26,17 @@ public class RetrieveTaskFromTaskListWebController { this.retrieveTaskFromTaskListUseCase = retrieveTaskFromTaskListUseCase; } + /** + * Retrieves a representation of task. Returns HTTP 200 OK if the request is successful with a + * representation of the task using the Content-Type "applicatoin/task+json". + * + * @param taskId the local identifier of the requested task (extracted from the task's URI) + * @return a representation of the task if the task exists + */ @GetMapping(path = "/tasks/{taskId}") public ResponseEntity retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) { - RetrieveTaskFromTaskListQuery command = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId)); - Optional updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(command); + RetrieveTaskFromTaskListQuery query = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId)); + Optional updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(query); // Check if the task with the given identifier exists if (updatedTaskOpt.isEmpty()) { @@ -30,11 +44,16 @@ public class RetrieveTaskFromTaskListWebController { throw new ResponseStatusException(HttpStatus.NOT_FOUND); } - // Add the content type as a response header - HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + try { + String taskRepresentation = TaskJsonRepresentation.serialize(updatedTaskOpt.get()); - return new ResponseEntity<>(TaskMediaType.serialize(updatedTaskOpt.get()), responseHeaders, - HttpStatus.OK); + // Add the content type as a response header + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); + + return new ResponseEntity<>(taskRepresentation, responseHeaders, HttpStatus.OK); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java new file mode 100644 index 0000000..7deb844 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskAssignedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapastasks.tasks.application.handler; + +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class TaskAssignedHandler implements TaskAssignedEventHandler { + + @Override + public Task handleTaskAssigned(TaskAssignedEvent taskAssignedEvent) throws TaskNotFoundException { + TaskList taskList = TaskList.getTapasTaskList(); + return taskList.changeTaskStatusToAssigned(taskAssignedEvent.getTaskId(), + taskAssignedEvent.getServiceProvider()); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java new file mode 100644 index 0000000..ec21e8c --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskExecutedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapastasks.tasks.application.handler; + +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class TaskExecutedHandler implements TaskExecutedEventHandler { + + @Override + public Task handleTaskExecuted(TaskExecutedEvent taskExecutedEvent) throws TaskNotFoundException { + TaskList taskList = TaskList.getTapasTaskList(); + return taskList.changeTaskStatusToExecuted(taskExecutedEvent.getTaskId(), + taskExecutedEvent.getServiceProvider(), taskExecutedEvent.getOutputData()); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java new file mode 100644 index 0000000..758be0b --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/handler/TaskStartedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.tapastasks.tasks.application.handler; + +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent; +import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException; +import org.springframework.stereotype.Component; + +@Component +public class TaskStartedHandler implements TaskStartedEventHandler { + + @Override + public Task handleTaskStarted(TaskStartedEvent taskStartedEvent) throws TaskNotFoundException { + TaskList taskList = TaskList.getTapasTaskList(); + return taskList.changeTaskStatusToRunning(taskStartedEvent.getTaskId(), + taskStartedEvent.getServiceProvider()); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java index a0e0fec..fbb66ed 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java @@ -1,23 +1,30 @@ package ch.unisg.tapastasks.tasks.application.port.in; import ch.unisg.tapastasks.common.SelfValidating; -import ch.unisg.tapastasks.tasks.domain.Task.TaskType; -import ch.unisg.tapastasks.tasks.domain.Task.TaskName; +import ch.unisg.tapastasks.tasks.domain.Task; +import lombok.Getter; import lombok.Value; import javax.validation.constraints.NotNull; +import java.util.Optional; @Value public class AddNewTaskToTaskListCommand extends SelfValidating { @NotNull - private final TaskName taskName; + private final Task.TaskName taskName; @NotNull - private final TaskType taskType; + private final Task.TaskType taskType; - public AddNewTaskToTaskListCommand(TaskName taskName, TaskType taskType) { + @Getter + private final Optional originalTaskUri; + + public AddNewTaskToTaskListCommand(Task.TaskName taskName, Task.TaskType taskType, + Optional originalTaskUri) { this.taskName = taskName; this.taskType = taskType; + this.originalTaskUri = originalTaskUri; + this.validateSelf(); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java index 40afc1d..cf7d787 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java @@ -5,5 +5,5 @@ import ch.unisg.tapastasks.tasks.domain.Task; import java.util.Optional; public interface RetrieveTaskFromTaskListUseCase { - Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command); + Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java new file mode 100644 index 0000000..c58d034 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java @@ -0,0 +1,25 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task; +import lombok.Getter; +import lombok.Value; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Value +public class TaskAssignedEvent extends SelfValidating { + @NotNull + private final Task.TaskId taskId; + + @Getter + private final Optional serviceProvider; + + public TaskAssignedEvent(Task.TaskId taskId, Optional serviceProvider) { + this.taskId = taskId; + this.serviceProvider = serviceProvider; + + this.validateSelf(); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java new file mode 100644 index 0000000..67f78dd --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEventHandler.java @@ -0,0 +1,8 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +public interface TaskAssignedEventHandler { + + Task handleTaskAssigned(TaskAssignedEvent taskStartedEvent); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java new file mode 100644 index 0000000..7ed9c84 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java @@ -0,0 +1,34 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task.*; +import lombok.Getter; +import lombok.Value; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Value +public class TaskExecutedEvent extends SelfValidating { + @NotNull + private final TaskId taskId; + + @Getter + private final Optional serviceProvider; + + @Getter + private final Optional outputData; + + public TaskExecutedEvent(TaskId taskId, Optional serviceProvider, + Optional outputData) { + this.taskId = taskId; + + this.serviceProvider = serviceProvider; + this.outputData = outputData; + + this.validateSelf(); + } + + + +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java new file mode 100644 index 0000000..c1a18dc --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEventHandler.java @@ -0,0 +1,8 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +public interface TaskExecutedEventHandler { + + Task handleTaskExecuted(TaskExecutedEvent taskExecutedEvent); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java new file mode 100644 index 0000000..8fad698 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEvent.java @@ -0,0 +1,28 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task; +import lombok.Getter; +import lombok.Value; + +import javax.validation.constraints.NotNull; +import java.util.Optional; + +@Value +public class TaskStartedEvent extends SelfValidating { + @NotNull + private final Task.TaskId taskId; + + @Getter + private final Optional serviceProvider; + + public TaskStartedEvent(Task.TaskId taskId, Optional serviceProvider) { + this.taskId = taskId; + this.serviceProvider = serviceProvider; + + this.validateSelf(); + } + + + +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java new file mode 100644 index 0000000..0da730e --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskStartedEventHandler.java @@ -0,0 +1,8 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +public interface TaskStartedEventHandler { + + Task handleTaskStarted(TaskStartedEvent taskStartedEvent); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java index 24f68d0..70818b1 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java @@ -21,7 +21,13 @@ public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase @Override public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) { TaskList taskList = TaskList.getTapasTaskList(); - Task newTask = taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType()); + + Task newTask = (command.getOriginalTaskUri().isPresent()) ? + // Create a delegated task that points back to the original task + taskList.addNewTaskWithNameAndTypeAndOriginalTaskUri(command.getTaskName(), + command.getTaskType(), command.getOriginalTaskUri().get()) + // Create an original task + : taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType()); //Here we are using the application service to emit the domain event to the outside of the bounded context. //This event should be considered as a light-weight "integration event" to communicate with other services. diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java index 46043b0..fd6aea5 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java @@ -15,8 +15,8 @@ import java.util.Optional; @Transactional public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase { @Override - public Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command) { + public Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query) { TaskList taskList = TaskList.getTapasTaskList(); - return taskList.retrieveTaskById(command.getTaskId()); + return taskList.retrieveTaskById(query.getTaskId()); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java index 3decd1f..ebe9d1c 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java @@ -8,7 +8,7 @@ import java.util.UUID; /**This is a domain entity**/ public class Task { - public enum State { + public enum Status { OPEN, ASSIGNED, RUNNING, EXECUTED } @@ -27,39 +27,85 @@ public class Task { @Getter public TaskResult taskResult; // same as above + // 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.taskResult = new TaskResult(""); + // 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; } @Value diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java index ccdc59a..7a4e70f 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java @@ -3,7 +3,6 @@ package ch.unisg.tapastasks.tasks.domain; import lombok.Getter; import lombok.Value; -import javax.swing.text.html.Option; import java.util.LinkedList; import java.util.List; import java.util.Optional; @@ -34,14 +33,27 @@ public class TaskList { //Only the aggregate root is allowed to create new tasks and add them to the task list. //Note: Here we could add some sophisticated invariants/business rules that the aggregate root checks public Task addNewTaskWithNameAndType(Task.TaskName name, Task.TaskType type) { - Task newTask = Task.createTaskWithNameAndType(name,type); - listOfTasks.value.add(newTask); - //This is a simple debug message to see that the task list is growing with each new request - System.out.println("Number of tasks: "+listOfTasks.value.size()); + Task newTask = Task.createTaskWithNameAndType(name, type); + this.addNewTaskToList(newTask); + + return newTask; + } + + public Task addNewTaskWithNameAndTypeAndOriginalTaskUri(Task.TaskName name, Task.TaskType type, + Task.OriginalTaskUri originalTaskUri) { + Task newTask = Task.createTaskWithNameAndTypeAndOriginalTaskUri(name, type, originalTaskUri); + this.addNewTaskToList(newTask); + + return newTask; + } + + private void addNewTaskToList(Task newTask) { //Here we would also publish a domain event to other entities in the core interested in this event. //However, we skip this here as it makes the core even more complex (e.g., we have to implement a light-weight //domain event publisher and subscribers (see "Implementing Domain-Driven Design by V. Vernon, pp. 296ff). - return newTask; + listOfTasks.value.add(newTask); + //This is a simple debug message to see that the task list is growing with each new request + System.out.println("Number of tasks: " + listOfTasks.value.size()); } public Optional retrieveTaskById(Task.TaskId id) { @@ -63,6 +75,41 @@ public class TaskList { } return Optional.empty(); + // public Task changeTaskStatusToAssigned(Task.TaskId id, Optional serviceProvider) + // throws TaskNotFoundException { + // return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty()); + // } + + // public Task changeTaskStatusToRunning(Task.TaskId id, Optional serviceProvider) + // throws TaskNotFoundException { + // return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty()); + // } + + // public Task changeTaskStatusToExecuted(Task.TaskId id, Optional serviceProvider, + // Optional outputData) throws TaskNotFoundException { + // return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData); + // } + + // private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional serviceProvider, + // Optional outputData) { + // Optional taskOpt = retrieveTaskById(id); + + // if (taskOpt.isEmpty()) { + // throw new TaskNotFoundException(); + // } + + // Task task = taskOpt.get(); + // task.setTaskStatus(status); + + // if (serviceProvider.isPresent()) { + // task.setProvider(serviceProvider.get()); + // } + + // if (outputData.isPresent()) { + // task.setOutputData(outputData.get()); + // } + + // return task; } @Value @@ -74,5 +121,4 @@ public class TaskList { public static class ListOfTasks { private List value; } - } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java new file mode 100644 index 0000000..830b934 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskNotFoundException.java @@ -0,0 +1,3 @@ +package ch.unisg.tapastasks.tasks.domain; + +public class TaskNotFoundException extends RuntimeException { } diff --git a/tapas-tasks/src/main/resources/application.properties b/tapas-tasks/src/main/resources/application.properties index 4d360de..fe25873 100644 --- a/tapas-tasks/src/main/resources/application.properties +++ b/tapas-tasks/src/main/resources/application.properties @@ -1 +1,2 @@ server.port=8081 +baseuri=https://tapas-tasks.86-119-34-23.nip.io/ From 06b418da8e4903ce6e278b8ae2454214cf69f325 Mon Sep 17 00:00:00 2001 From: Andrei Ciortea Date: Thu, 21 Oct 2021 14:58:20 +0200 Subject: [PATCH 05/40] Fix redirect for tapas-auction-house --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index b4c85e2..0081933 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -50,7 +50,7 @@ services: 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.routers.tapas-auction-house.service=tapas-auction-house" - "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" From 5c82dd84b44ea23f74d5699c1a9759ae1d0f654c Mon Sep 17 00:00:00 2001 From: Andrei Ciortea Date: Fri, 22 Oct 2021 12:39:50 +0200 Subject: [PATCH 06/40] Add instructions for setting up a local WebSub hub and MQTT broker --- tapas-auction-house/BROKERS.md | 121 +++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 tapas-auction-house/BROKERS.md diff --git a/tapas-auction-house/BROKERS.md b/tapas-auction-house/BROKERS.md new file mode 100644 index 0000000..7e0d37a --- /dev/null +++ b/tapas-auction-house/BROKERS.md @@ -0,0 +1,121 @@ +# Event-based Interaction with W3C WebSub and MQTT + +## Setting up a local WebSub Hub + +W3C WebSub Recommendation: [https://www.w3.org/TR/websub/](https://www.w3.org/TR/websub/) + +There are several implementations of W3C WebSub available. One implementation that is easy to set up is: +[https://github.com/hemerajs/websub-hub](https://github.com/hemerajs/websub-hub) + +Running this WebSub Hub implementation requires Docker, Node.js, and npm: +* installation instructions for Docker: [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/) +* installation instructions for Node.js and npm: [https://docs.npmjs.com/downloading-and-installing-node-js-and-npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) + +### How to run + +```shell +git clone https://github.com/hemerajs/websub-hub.git +cd websub-hub +docker run -d -p 27017:27017 -p 28017:28017 -e AUTH=no tutum/mongodb +npm i -g websub-hub-cli +websub-hub -l info -m mongodb://localhost:27017/hub +``` + +The third command launches a Docker container with a [MongoDB](https://www.mongodb.com/) instance, +which is used to persist all subscriptions made with the hub. See the README of the original project +for further details: [https://github.com/hemerajs/websub-hub](https://github.com/hemerajs/websub-hub) + +### Implementation note on verification of intents + +There are 3 main W3C WebSub hub implementations out of which: +* 2 are public but closed-source: [Google' WebHub hub](http://pubsubhubbub.appspot.com/) +and [Superfeedr's WebSub hub](https://websub.superfeedr.com/) +* 1 is public and open-source, but it requires additional overhead to set up for local development: + [Switchboard](https://switchboard.p3k.io/) + +The project we recommend above provides by far one of the easiest ways to run a W3C WebSub Hub locally. However, +**interoperability is hard**: this project diverges in one significant way from the [W3C WebSub Recommendation](https://www.w3.org/TR/websub/) +and the main [WebSub Hub](http://pubsubhubbub.appspot.com/) implementations. To save you from the +interoperability headaches, we document this divergence below. + +#### Verifying subscriber intents + +When a Subscriber registers with a WebSub Hub, the Hub is required to verify the intent of the subscriber +in order to prevent an attacker from creating unwanted subscriptions. + +To verify the Subscriber's intent, the Hub sends an HTTP GET request to the subscriber's callback +URL. The HTTP GET request includes several parameters, one of which is a hub-generated random string +with the name `hub.challenge`. To confirm the subscription, a Susbcriber must then respond with an +HTTP 2xx status code and a response body equal to the `hub.challenge` parameter. For more details on +the verification of intents, see [Section 5.3 in the W3C WebSub Recommendation](https://www.w3.org/TR/websub/#hub-verifies-intent). + +#### Verifying subscriber intents with the Hemerajs implementation + +The above Hemerajs implementation differs from the W3C WebSub Recommendation in that the Hub expects the +response body confirming the intent to be in the JSON format with the `application/json` media type. + +Sample HTTP request and the HTTP response expected by the Hemerajs WeSub Hub: + +```shell +curl -i --location --request GET 'localhost:8084/websub/?hub.mode=subscribe&hub.topic=http://example.org&hub.challenge=hub-generated-challenge' + +HTTP/1.1 200 +Content-Type: application/json +Content-Length: 46 +Date: Fri, 22 Oct 2021 10:14:03 GMT + +{ + "hub.challenge": "hub-generated-challenge" +} +``` + +#### Verifying subscriber intents in a standard W3C WebSub implementation + +In contrast to the Hemerajs implementation, a standard WebSub Hub implementation would require the reponse body to be exactly equal to the +hub-generated challenge parameter. Here is an example of a standard response, as expected by WebSub Hubs +that are fully standard-compliant: + +```shell +curl -i --location --request GET 'https://websub.flus.io/dummy-subscriber?hub.mode=subscribe&hub.topic=http://example.org&hub.challenge=hub-generated-challenge' + +HTTP/2 200 +server: nginx +date: Fri, 22 Oct 2021 10:15:58 GMT +content-type: text/plain;charset=UTF-8 +content-security-policy: default-src 'self' +strict-transport-security: max-age=63072000 + +hub-generated-challenge +``` + +#### What this means for your TAPAS application + +We recommend using the Hemerajs WebSub Hub implementation for local development. + +In our deployment, however, we will use one of the publicly available W3C WebSub hubs. You will then +have to change the response for intent verification to match the standard response: that is, to return +directly the `hub.challenge` parameter as shown above. + + +## Setting up a local MQTT broker + +An easy way to set up a local MQTT broker is with HiveMQ and Docker: +[https://www.hivemq.com/downloads/docker/](https://www.hivemq.com/downloads/docker/) + +```shell +docker run -p 8080:8080 -p 1883:1883 hivemq/hivemq4 +``` + +The above command launches a Docker container with a HiveMQT broker and binds to the container on 2 ports: +* port `1883` is used by the MQTT protocol +* port `8080` is used for the HiveMQ dashboard; point your browser to: [http://localhost:8080/](http://localhost:8080/) + +To bind the Docker container to a different HTTP port, you can configure the first parameter. E.g., +this command binds the HiveMQT dashboard to port `8085`: + +```shell +docker run -p 8085:8080 -p 1883:1883 hivemq/hivemq4 +``` + +For development and debugging, it might help to install an MQTT client as well. HiveMQ provides an MQTT +Command-Line Interface (CLI) that may help: [https://hivemq.github.io/mqtt-cli/](https://hivemq.github.io/mqtt-cli/) From 67524548385fe56e1889008e3785c61a0ce0576b Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 27 Oct 2021 13:15:29 +0200 Subject: [PATCH 07/40] added common lib and added service uri's to properties file --- assignment/pom.xml | 6 + .../in/web/ApplyForTaskController.java | 2 +- .../in/web/WebControllerExceptionHandler.java | 18 +- ...llExecutorInExecutorPoolByTypeAdapter.java | 15 +- .../out/web/PublishNewTaskEventAdapter.java | 20 +- .../web/PublishTaskAssignedEventAdapter.java | 9 +- .../web/PublishTaskCompletedEventAdapter.java | 9 +- .../port/in/ApplyForTaskCommand.java | 16 +- .../application/port/in/NewTaskCommand.java | 2 +- .../port/in/TaskCompletedCommand.java | 2 +- .../service/ApplyForTaskService.java | 2 +- .../application/service/NewTaskService.java | 4 - .../assignment/domain/ExecutorInfo.java | 9 +- .../assignment/assignment/domain/Roster.java | 7 +- .../assignment/domain/RosterItem.java | 14 +- .../domain/valueobject/IP4Adress.java | 23 - .../assignment/domain/valueobject/Port.java | 17 - .../common/exception/InvalidIP4Exception.java | 7 - .../exception/PortOutOfRangeException.java | 7 - .../src/main/resources/application.properties | 4 + .../.mvn/wrapper/MavenWrapperDownloader.java | 117 ++++ common/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes common/.mvn/wrapper/maven-wrapper.properties | 2 + common/mvnw | 310 +++++++++ common/mvnw.cmd | 182 +++++ common/pom.xml | 72 ++ .../common/exception/ErrorResponse.java | 2 +- .../InvalidExecutorURIException.java | 7 + .../common/validation}/SelfValidating.java | 2 +- .../unisg/common/valueobject/ExecutorURI.java | 18 + diagram_1.bpmn | 653 ------------------ doc/workflow.bpmn | 337 +++++++++ doc/workflow.png | Bin 0 -> 836841 bytes executor-base/pom.xml | 6 + .../executorBase/Executor1Application.java | 13 - .../executorBase/common/SelfValidating.java | 30 - .../in/web/TaskAvailableController.java | 8 +- .../web/ExecutionFinishedEventAdapter.java | 15 +- .../adapter/out/web/GetAssignmentAdapter.java | 23 +- .../out/web/NotifyExecutorPoolAdapter.java | 21 +- .../port/in/TaskAvailableCommand.java | 7 +- .../port/in/TaskAvailableUseCase.java | 2 +- .../port/out/ExecutionFinishedEventPort.java | 4 +- .../port/out/GetAssignmentPort.java | 9 +- .../port/out/NotifyExecutorPoolPort.java | 7 +- .../service/NotifyExecutorPoolService.java | 11 +- .../service/TaskAvailableService.java | 6 +- .../domain/ExecutionFinishedEvent.java | 6 +- .../executor/domain/ExecutorBase.java | 38 +- .../executor/domain/ExecutorStatus.java | 2 +- .../executor/domain/ExecutorType.java | 2 +- .../executorBase/executor/domain/Task.java | 2 +- .../src/main/resources/application.properties | 5 + .../Executor1ApplicationTests.java | 13 - .../in/web/TaskAvailableController.java | 6 +- .../application/port/out/UserToRobotPort.java | 2 +- .../service/TaskAvailableService.java | 8 +- .../executor1/executor/domain/Executor.java | 4 +- .../in/web/TaskAvailableController.java | 6 +- .../service/TaskAvailableService.java | 6 +- .../executor2/executor/domain/Executor.java | 5 +- 61 files changed, 1231 insertions(+), 931 deletions(-) delete mode 100644 assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/IP4Adress.java delete mode 100644 assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/Port.java delete mode 100644 assignment/src/main/java/ch/unisg/assignment/common/exception/InvalidIP4Exception.java delete mode 100644 assignment/src/main/java/ch/unisg/assignment/common/exception/PortOutOfRangeException.java create mode 100644 common/.mvn/wrapper/MavenWrapperDownloader.java create mode 100644 common/.mvn/wrapper/maven-wrapper.jar create mode 100644 common/.mvn/wrapper/maven-wrapper.properties create mode 100755 common/mvnw create mode 100644 common/mvnw.cmd create mode 100644 common/pom.xml rename {assignment/src/main/java/ch/unisg/assignment => common/src/main/java/ch/unisg}/common/exception/ErrorResponse.java (84%) create mode 100644 common/src/main/java/ch/unisg/common/exception/InvalidExecutorURIException.java rename {assignment/src/main/java/ch/unisg/assignment/common => common/src/main/java/ch/unisg/common/validation}/SelfValidating.java (95%) create mode 100644 common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java delete mode 100644 diagram_1.bpmn create mode 100644 doc/workflow.bpmn create mode 100644 doc/workflow.png delete mode 100644 executor-base/src/main/java/ch/unisg/executorBase/Executor1Application.java delete mode 100644 executor-base/src/main/java/ch/unisg/executorBase/common/SelfValidating.java delete mode 100644 executor-base/src/test/java/ch/unisg/executorBase/Executor1ApplicationTests.java diff --git a/assignment/pom.xml b/assignment/pom.xml index 99996b8..b4650de 100644 --- a/assignment/pom.xml +++ b/assignment/pom.xml @@ -62,6 +62,12 @@ 20210307 + + ch.unisg + common + 0.0.1-SNAPSHOT + + diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java index 1d0111d..c77f6f9 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java @@ -21,7 +21,7 @@ public class ApplyForTaskController { public Task applyForTask(@RequestBody ExecutorInfo executorInfo) { ApplyForTaskCommand command = new ApplyForTaskCommand(executorInfo.getExecutorType(), - executorInfo.getIp(), executorInfo.getPort()); + executorInfo.getExecutorURI()); return applyForTaskUseCase.applyForTask(command); diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java index 08a0895..99ad2a5 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java @@ -5,27 +5,17 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import ch.unisg.assignment.common.exception.ErrorResponse; -import ch.unisg.assignment.common.exception.InvalidIP4Exception; -import ch.unisg.assignment.common.exception.PortOutOfRangeException; +import ch.unisg.common.exception.ErrorResponse; +import ch.unisg.common.exception.InvalidExecutorURIException; @ControllerAdvice public class WebControllerExceptionHandler { - @ExceptionHandler(PortOutOfRangeException.class) - public ResponseEntity handleException(PortOutOfRangeException e){ + @ExceptionHandler(InvalidExecutorURIException.class) + public ResponseEntity handleException(InvalidExecutorURIException e){ ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST, e.getLocalizedMessage()); return new ResponseEntity<>(error, error.getHttpStatus()); } - - @ExceptionHandler(InvalidIP4Exception.class) - public ResponseEntity handleException(InvalidIP4Exception e){ - - ErrorResponse error = new ErrorResponse(HttpStatus.BAD_REQUEST, e.getLocalizedMessage()); - return new ResponseEntity<>(error, error.getHttpStatus()); - - } - } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java index 4163a53..0a91805 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java @@ -9,7 +9,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONArray; -import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -21,9 +21,11 @@ import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; @Primary public class GetAllExecutorInExecutorPoolByTypeAdapter implements GetAllExecutorInExecutorPoolByTypePort { + @Value("${executor-pool.url}") + private String server; + @Override public boolean doesExecutorTypeExist(ExecutorType type) { - String server = "http://127.0.0.1:8083"; Logger logger = Logger.getLogger(PublishNewTaskEventAdapter.class.getName()); @@ -37,17 +39,18 @@ public class GetAllExecutorInExecutorPoolByTypeAdapter implements GetAllExecutor try { - HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() == HttpStatus.OK.value()) { - JSONArray jsonArray = new JSONArray(response.body().toString()); + JSONArray jsonArray = new JSONArray(response.body()); if (jsonArray.length() > 0) { return true; } } - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } return false; } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java index 5007c1c..db3de1c 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java @@ -8,6 +8,7 @@ import java.net.http.HttpResponse; import java.util.logging.Level; import java.util.logging.Logger; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @@ -18,8 +19,11 @@ import ch.unisg.assignment.assignment.domain.event.NewTaskEvent; @Primary public class PublishNewTaskEventAdapter implements NewTaskEventPort { - String server = "http://127.0.0.1:8084"; - String server2 = "http://127.0.0.1:8085"; + @Value("${executor1.url}") + private String server; + + @Value("${executor2.url}") + private String server2; Logger logger = Logger.getLogger(PublishNewTaskEventAdapter.class.getName()); @@ -35,10 +39,11 @@ public class PublishNewTaskEventAdapter implements NewTaskEventPort { try { client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } HttpClient client2 = HttpClient.newHttpClient(); @@ -49,11 +54,12 @@ public class PublishNewTaskEventAdapter implements NewTaskEventPort { try { - client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException | InterruptedException e) { + client2.send(request2, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java index 56cb803..209525e 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java @@ -9,6 +9,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @@ -19,7 +20,8 @@ import ch.unisg.assignment.assignment.domain.event.TaskAssignedEvent; @Primary public class PublishTaskAssignedEventAdapter implements TaskAssignedEventPort { - String server = "http://127.0.0.1:8081"; + @Value("${task-list.url}") + private String server; Logger logger = Logger.getLogger(PublishTaskAssignedEventAdapter.class.getName()); @@ -40,10 +42,11 @@ public class PublishTaskAssignedEventAdapter implements TaskAssignedEventPort { try { client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java index f9f2833..6bd56a0 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java @@ -9,6 +9,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @@ -19,7 +20,8 @@ import ch.unisg.assignment.assignment.domain.event.TaskCompletedEvent; @Primary public class PublishTaskCompletedEventAdapter implements TaskCompletedEventPort { - String server = "http://127.0.0.1:8081"; + @Value("${task-list.url}") + private String server; Logger logger = Logger.getLogger(PublishTaskCompletedEventAdapter.class.getName()); @@ -42,10 +44,11 @@ public class PublishTaskCompletedEventAdapter implements TaskCompletedEventPort try { client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskCommand.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskCommand.java index df36d58..bdc16d9 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskCommand.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskCommand.java @@ -3,9 +3,8 @@ package ch.unisg.assignment.assignment.application.port.in; import javax.validation.constraints.NotNull; import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; -import ch.unisg.assignment.assignment.domain.valueobject.IP4Adress; -import ch.unisg.assignment.assignment.domain.valueobject.Port; -import ch.unisg.assignment.common.SelfValidating; +import ch.unisg.common.validation.SelfValidating; +import ch.unisg.common.valueobject.ExecutorURI; import lombok.EqualsAndHashCode; import lombok.Value; @@ -17,16 +16,11 @@ public class ApplyForTaskCommand extends SelfValidating{ private final ExecutorType taskType; @NotNull - private final IP4Adress executorIP; + private final ExecutorURI executorURI; - - @NotNull - private final Port executorPort; - - public ApplyForTaskCommand(ExecutorType taskType, IP4Adress executorIP, Port executorPort) { + public ApplyForTaskCommand(ExecutorType taskType, ExecutorURI executorURI) { this.taskType = taskType; - this.executorIP = executorIP; - this.executorPort = executorPort; + this.executorURI = executorURI; this.validateSelf(); } } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskCommand.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskCommand.java index ab6838e..f06798b 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskCommand.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskCommand.java @@ -3,7 +3,7 @@ package ch.unisg.assignment.assignment.application.port.in; import javax.validation.constraints.NotNull; import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; -import ch.unisg.assignment.common.SelfValidating; +import ch.unisg.common.validation.SelfValidating; import lombok.EqualsAndHashCode; import lombok.Value; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedCommand.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedCommand.java index b0af2b4..08dc8eb 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedCommand.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedCommand.java @@ -2,7 +2,7 @@ package ch.unisg.assignment.assignment.application.port.in; import javax.validation.constraints.NotNull; -import ch.unisg.assignment.common.SelfValidating; +import ch.unisg.common.validation.SelfValidating; import lombok.EqualsAndHashCode; import lombok.Value; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java index 0593a30..5ba1901 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java @@ -22,7 +22,7 @@ public class ApplyForTaskService implements ApplyForTaskUseCase { @Override public Task applyForTask(ApplyForTaskCommand command) { Task task = Roster.getInstance().assignTaskToExecutor(command.getTaskType(), - command.getExecutorIP(), command.getExecutorPort()); + command.getExecutorURI()); if (task != null) { taskAssignedEventPort.publishTaskAssignedEvent(new TaskAssignedEvent(task.getTaskID())); diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java index 7d7de5c..8f60789 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java @@ -1,8 +1,5 @@ package ch.unisg.assignment.assignment.application.service; -import java.util.Arrays; -import java.util.List; - import javax.transaction.Transactional; import org.springframework.stereotype.Component; @@ -27,7 +24,6 @@ public class NewTaskService implements NewTaskUseCase { @Override public boolean addNewTaskToQueue(NewTaskCommand command) { - // TODO Get availableTaskTypes from executor pool if (!getAllExecutorInExecutorPoolByTypePort.doesExecutorTypeExist(command.getTaskType())) { return false; } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/ExecutorInfo.java b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/ExecutorInfo.java index 6b19dcc..58b47dc 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/ExecutorInfo.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/ExecutorInfo.java @@ -1,19 +1,14 @@ package ch.unisg.assignment.assignment.domain; import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; -import ch.unisg.assignment.assignment.domain.valueobject.IP4Adress; -import ch.unisg.assignment.assignment.domain.valueobject.Port; +import ch.unisg.common.valueobject.ExecutorURI; import lombok.Getter; import lombok.Setter; public class ExecutorInfo { @Getter @Setter - private IP4Adress ip; - - @Getter - @Setter - private Port port; + private ExecutorURI executorURI; @Getter @Setter diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java index 521a748..560d7fc 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java @@ -5,8 +5,7 @@ import java.util.Arrays; import java.util.HashMap; import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; -import ch.unisg.assignment.assignment.domain.valueobject.IP4Adress; -import ch.unisg.assignment.assignment.domain.valueobject.Port; +import ch.unisg.common.valueobject.ExecutorURI; public class Roster { @@ -30,7 +29,7 @@ public class Roster { } } - public Task assignTaskToExecutor(ExecutorType taskType, IP4Adress executorIP, Port executorPort) { + public Task assignTaskToExecutor(ExecutorType taskType, ExecutorURI executorURI) { if (!queues.containsKey(taskType.getValue())) { return null; } @@ -41,7 +40,7 @@ public class Roster { Task task = queues.get(taskType.getValue()).remove(0); rosterMap.put(task.getTaskID(), new RosterItem(task.getTaskID(), - task.getTaskType().getValue(), executorIP, executorPort)); + task.getTaskType().getValue(), executorURI)); return task; } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/RosterItem.java b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/RosterItem.java index 2c3bb52..b405f44 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/RosterItem.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/RosterItem.java @@ -1,7 +1,6 @@ package ch.unisg.assignment.assignment.domain; -import ch.unisg.assignment.assignment.domain.valueobject.IP4Adress; -import ch.unisg.assignment.assignment.domain.valueobject.Port; +import ch.unisg.common.valueobject.ExecutorURI; import lombok.Getter; public class RosterItem { @@ -13,17 +12,12 @@ public class RosterItem { private String taskType; @Getter - private IP4Adress executorIP; + private ExecutorURI executorURI; - @Getter - private Port executorPort; - - - public RosterItem(String taskID, String taskType, IP4Adress executorIP, Port executorPort) { + public RosterItem(String taskID, String taskType, ExecutorURI executorURI) { this.taskID = taskID; this.taskType = taskType; - this.executorIP = executorIP; - this.executorPort = executorPort; + this.executorURI = executorURI; } } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/IP4Adress.java b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/IP4Adress.java deleted file mode 100644 index cd23b6b..0000000 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/IP4Adress.java +++ /dev/null @@ -1,23 +0,0 @@ -package ch.unisg.assignment.assignment.domain.valueobject; - -import ch.unisg.assignment.common.exception.InvalidIP4Exception; -import lombok.Value; - -@Value -public class IP4Adress { - private String value; - - public IP4Adress(String ip4) throws InvalidIP4Exception { - if (ip4.equalsIgnoreCase("localhost") || - ip4.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$")) { - this.value = ip4; - } else { - throw new InvalidIP4Exception(); - } - } -} - - - - - diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/Port.java b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/Port.java deleted file mode 100644 index a66dbbd..0000000 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/Port.java +++ /dev/null @@ -1,17 +0,0 @@ -package ch.unisg.assignment.assignment.domain.valueobject; - -import ch.unisg.assignment.common.exception.PortOutOfRangeException; -import lombok.Value; - -@Value -public class Port { - private int value; - - public Port(int port) throws PortOutOfRangeException { - if (1024 <= port && port <= 65535) { - this.value = port; - } else { - throw new PortOutOfRangeException(); - } - } -} diff --git a/assignment/src/main/java/ch/unisg/assignment/common/exception/InvalidIP4Exception.java b/assignment/src/main/java/ch/unisg/assignment/common/exception/InvalidIP4Exception.java deleted file mode 100644 index fecbfcb..0000000 --- a/assignment/src/main/java/ch/unisg/assignment/common/exception/InvalidIP4Exception.java +++ /dev/null @@ -1,7 +0,0 @@ -package ch.unisg.assignment.common.exception; - -public class InvalidIP4Exception extends Exception { - public InvalidIP4Exception() { - super("IP4 is invalid"); - } -} diff --git a/assignment/src/main/java/ch/unisg/assignment/common/exception/PortOutOfRangeException.java b/assignment/src/main/java/ch/unisg/assignment/common/exception/PortOutOfRangeException.java deleted file mode 100644 index 2772256..0000000 --- a/assignment/src/main/java/ch/unisg/assignment/common/exception/PortOutOfRangeException.java +++ /dev/null @@ -1,7 +0,0 @@ -package ch.unisg.assignment.common.exception; - -public class PortOutOfRangeException extends Exception { - public PortOutOfRangeException() { - super("Port is out of available range (1024-65535)"); - } -} diff --git a/assignment/src/main/resources/application.properties b/assignment/src/main/resources/application.properties index 3cf12af..dc443ab 100644 --- a/assignment/src/main/resources/application.properties +++ b/assignment/src/main/resources/application.properties @@ -1 +1,5 @@ server.port=8082 +executor-pool.url=http://127.0.0.1:8083 +executor1.url=http://127.0.0.1:8084 +executor2.url=http://127.0.0.1:8085 +task-list.url=http://127.0.0.1:8081 diff --git a/common/.mvn/wrapper/MavenWrapperDownloader.java b/common/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..e76d1f3 --- /dev/null +++ b/common/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/common/.mvn/wrapper/maven-wrapper.jar b/common/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..2cc7d4a55c0cd0092912bf49ae38b3a9e3fd0054 GIT binary patch literal 50710 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 EfAhh=DF6Tf literal 0 HcmV?d00001 diff --git a/common/.mvn/wrapper/maven-wrapper.properties b/common/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..abd303b --- /dev/null +++ b/common/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.2/apache-maven-3.8.2-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/common/mvnw b/common/mvnw new file mode 100755 index 0000000..a16b543 --- /dev/null +++ b/common/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/common/mvnw.cmd b/common/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/common/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/common/pom.xml b/common/pom.xml new file mode 100644 index 0000000..f4e8342 --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,72 @@ + + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.5 + + + + ch.unisg + common + 0.0.1-SNAPSHOT + + common + + http://www.example.com + + + 11 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + javax.validation + validation-api + 1.1.0.Final + + + + javax.transaction + javax.transaction-api + 1.2 + + + + org.json + json + 20210307 + + + + + diff --git a/assignment/src/main/java/ch/unisg/assignment/common/exception/ErrorResponse.java b/common/src/main/java/ch/unisg/common/exception/ErrorResponse.java similarity index 84% rename from assignment/src/main/java/ch/unisg/assignment/common/exception/ErrorResponse.java rename to common/src/main/java/ch/unisg/common/exception/ErrorResponse.java index 2fb834e..aeef41c 100644 --- a/assignment/src/main/java/ch/unisg/assignment/common/exception/ErrorResponse.java +++ b/common/src/main/java/ch/unisg/common/exception/ErrorResponse.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.common.exception; +package ch.unisg.common.exception; import org.springframework.http.HttpStatus; diff --git a/common/src/main/java/ch/unisg/common/exception/InvalidExecutorURIException.java b/common/src/main/java/ch/unisg/common/exception/InvalidExecutorURIException.java new file mode 100644 index 0000000..1d619e9 --- /dev/null +++ b/common/src/main/java/ch/unisg/common/exception/InvalidExecutorURIException.java @@ -0,0 +1,7 @@ +package ch.unisg.common.exception; + +public class InvalidExecutorURIException extends Exception { + public InvalidExecutorURIException() { + super("URI is invalid"); + } +} diff --git a/assignment/src/main/java/ch/unisg/assignment/common/SelfValidating.java b/common/src/main/java/ch/unisg/common/validation/SelfValidating.java similarity index 95% rename from assignment/src/main/java/ch/unisg/assignment/common/SelfValidating.java rename to common/src/main/java/ch/unisg/common/validation/SelfValidating.java index a8d366f..bb2d0fe 100644 --- a/assignment/src/main/java/ch/unisg/assignment/common/SelfValidating.java +++ b/common/src/main/java/ch/unisg/common/validation/SelfValidating.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.common; +package ch.unisg.common.validation; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; diff --git a/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java b/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java new file mode 100644 index 0000000..fc6b62d --- /dev/null +++ b/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java @@ -0,0 +1,18 @@ +package ch.unisg.common.valueobject; + +import ch.unisg.common.exception.InvalidExecutorURIException; +import lombok.Value; + +@Value +public class ExecutorURI { + private String value; + + public ExecutorURI(String uri) throws InvalidExecutorURIException { + if (uri.equalsIgnoreCase("localhost") || + uri.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$")) { + this.value = uri; + } else { + throw new InvalidExecutorURIException(); + } + } +} diff --git a/diagram_1.bpmn b/diagram_1.bpmn deleted file mode 100644 index 313a860..0000000 --- a/diagram_1.bpmn +++ /dev/null @@ -1,653 +0,0 @@ - - - - - - - - - - - - - - Gateway_1vd3as7 - Activity_1jd11bs - Gateway_1e4ckdq - Activity_1dl4fvt - Gateway_1e3rabp - Event_1lm6x5y - Event_1ysgenb - Event_00rvb9o - Activity_0u3tts0 - Activity_0vs4eam - Activity_0mwpp9o - Event_0v7hm2z - Activity_0paecdb - Activity_0srcl99 - Activity_0assw9c - - - Activity_0jai885 - StartEvent_1 - - - - Flow_1hc51tx - Flow_1sajzlx - Flow_1ijfkpz - - - Flow_1sajzlx - Flow_0cpd5ad - - - Flow_0cpd5ad - Flow_0kwvrmc - Flow_1w3uh2m - - - Flow_0kwvrmc - Flow_0vpcut0 - - - Flow_0vpcut0 - Flow_179e0hl - Flow_12nh0g5 - - - Flow_179e0hl - - - Flow_0366zqm - - - Flow_1n8jm89 - - - Flow_1w3uh2m - Flow_1n8jm89 - - - Flow_19dbo28 - Flow_1rwgf2n - - - Flow_19dbo28 - - - Flow_1ijfkpz - Flow_1gvdy5x - - - Flow_1gvdy5x - Flow_1yxp4e8 - - - Flow_1yxp4e8 - - - Flow_1rwgf2n - Flow_1hc51tx - - - - Flow_12nh0g5 - Flow_0366zqm - - - - - - - - - - - - - - - - - - - - Flow_119ldsr - - - Flow_1mm9swr - - - Flow_119ldsr - Flow_1mm9swr - - - - - - - - Event_0v73rhy - Activity_0n8uuvk - - - Activity_06xjrrk - - - Activity_0uxkytf - Event_01nh9j2 - Activity_1qgjnyh - - - - Flow_17d0j42 - - - Flow_17d0j42 - Flow_0opy5tp - - - Flow_0opy5tp - Flow_0sudw7l - - - Flow_0sudw7l - Flow_02uzxx3 - - - Flow_0rpv16j - - - Flow_02uzxx3 - Flow_0rpv16j - - - - - - - - - - - Event_1oz3tr5 - Activity_0xk9kck - - - Gateway_079742h - Activity_0umieiz - Event_062x6a9 - Activity_0b6bh6v - Event_1achffx - Activity_1mme68o - - - - Flow_0od6iot - - - Flow_0od6iot - Flow_0k07ofo - - - Flow_0k07ofo - Flow_1fwmda3 - Flow_050yku1 - - - Flow_1fwmda3 - Flow_15lkkxa - - - Flow_15lkkxa - - - Flow_050yku1 - Flow_1lgcq8d - - - Flow_0oqra8s - - - Flow_1lgcq8d - Flow_0oqra8s - - - - - - - - - - - - - Event_1b2jt5y - Activity_1ikvmpl - Event_0y66dbe - Activity_11lqrhg - Activity_1oownmu - - - Activity_1ohwol1 - Event_0bbqq3x - - - Activity_1e9hb9h - Event_0l7ljcr - - - - Flow_046hsk4 - - - Flow_046hsk4 - Flow_0zoqmub - - - Flow_0nyxv8e - - - Flow_0zoqmub - Flow_0ip5x3i - Flow_1bqiz65 - Flow_142xsjm - - - Flow_1bqiz65 - Flow_0znvsoe - - - Flow_142xsjm - Flow_0ue2i8v - - - Flow_0ue2i8v - - - Flow_0znvsoe - - - Flow_0ip5x3i - Flow_0nyxv8e - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/workflow.bpmn b/doc/workflow.bpmn new file mode 100644 index 0000000..6d68a24 --- /dev/null +++ b/doc/workflow.bpmn @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + Flow_1rie16h + Flow_159tlyd + Flow_01pbz6s + + + Flow_159tlyd + + + Flow_01pbz6s + + + Flow_1urp3d2 + + + Flow_1urp3d2 + Flow_1oy6e8u + Flow_1rxws1j + + + Flow_1oy6e8u + + + Flow_1rxws1j + + + + + Flow_1rie16h + + + + + + + + + + + + + + + Flow_0nuuhk7 + + + Flow_0nuuhk7 + Flow_197gie6 + Flow_0ruufha + + + Flow_197gie6 + + + + Flow_0ruufha + Flow_1duwugb + + + + Flow_19m4xhk + + + Flow_19m4xhk + Flow_0b4g73l + Flow_1duwugb + + + Flow_0b4g73l + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/workflow.png b/doc/workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..49466939aa20738e5423de482de84e821241f043 GIT binary patch literal 836841 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

Hello, world!

', + url: 'https://example.org/initial-post' + } + ] +}) + +fastify.get('/websub', async (request, reply) => { + console.log('content provided') + + return { + topic: 'http://localhost:3100/auctions' + } +}) + +// Run the server! +const start = async () => { + try { + await fastify.listen(3100) + } catch (err) { + fastify.log.error(err) + process.exit(1) + } +} +start() diff --git a/mocks/auction-house/publisher.js b/mocks/auction-house/publisher.js new file mode 100644 index 0000000..c760820 --- /dev/null +++ b/mocks/auction-house/publisher.js @@ -0,0 +1,17 @@ +const axios = require('axios').default + +// Run the server! +const start = async () => { + await axios + .post('http://localhost:3000/publish', { + 'hub.mode': 'publish', + 'hub.url': 'http://localhost:3100/auctions' + }) + .then(response => { + console.log(response.data) + }) + .catch(error => { + console.log(error) + }) +} +start() diff --git a/mocks/auction-house/subscriber.js b/mocks/auction-house/subscriber.js new file mode 100644 index 0000000..ce27197 --- /dev/null +++ b/mocks/auction-house/subscriber.js @@ -0,0 +1,42 @@ +// Require the framework and instantiate it +const fastify = require('fastify')({ logger: true }) +const axios = require('axios').default + +// Declare a route +fastify.get('/auction-created', async (request, reply) => { + console.log('subscription verified', request.query) + console.log(request.query) + return request.query +}) + +fastify.post('/auction-created', async (request, reply) => { + console.log('received blog content', request.body) + reply.send() +}) + +// Run the server! +const start = async () => { + // subscribe to the feed + + try { + await fastify.listen(3200) + } catch (err) { + fastify.log.error(err) + process.exit(1) + } + + await axios + .post('http://localhost:3000', { + 'hub.callback': 'http://localhost:3200/auction-created', + 'hub.mode': 'subscribe', + 'hub.topic': 'http://localhost:3100/auctions', + 'hub.ws': false + }) + .then(response => { + console.log(response.data) + }) + .catch(error => { + console.log(error) + }) +} +start() diff --git a/tapas-auction-house/pom.xml b/tapas-auction-house/pom.xml index 4b9cbb6..df44681 100644 --- a/tapas-auction-house/pom.xml +++ b/tapas-auction-house/pom.xml @@ -58,6 +58,17 @@ validation-api 1.1.0.Final + + org.json + json + 20210307 + + + org.springframework.boot + spring-boot-devtools + runtime + true + diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java index 8fc22d0..3459cff 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -7,6 +7,7 @@ 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.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -26,17 +27,21 @@ public class TapasAuctionHouseApplication { 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); + + // We will use these bootstrap methods in Week 6: + + // bootstrapMarketplaceWithMqtt(); + bootstrapMarketplaceWithWebSub(); } /** * Discovers auction houses and subscribes to WebSub notifications */ private static void bootstrapMarketplaceWithWebSub() { + System.out.println("HAHA"); List auctionHouseEndpoints = discoverAuctionHouseEndpoints(); LOGGER.info("Found auction house endpoints: " + auctionHouseEndpoints); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java index da2b096..8066010 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java @@ -1,6 +1,18 @@ package ch.unisg.tapas.auctionhouse.adapter.common.clients; +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.logging.Level; +import java.util.logging.Logger; + +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; /** * Subscribes to the WebSub hubs of auction houses discovered at run time. This class is instantiated @@ -9,7 +21,23 @@ import java.net.URI; */ public class WebSubSubscriber { + // TODO get this somehow from properties file. But on clue how to do this with static variables + static String WEBSUB_HUB_ENDPOINT = "http://localhost:3000"; + static String AUCTION_HOUSE_ENDPOINT = "http://localhost:8086"; + + Logger logger = Logger.getLogger(WebSubSubscriber.class.getName()); + public void subscribeToAuctionHouseEndpoint(URI endpoint) { + // TODO decide with other groups about auction house endpoint uri to discover websub topics + // and replace the hardcoded one with it + String topic = discoverWebSubTopic("http://localhost:3100/websub"); + + if (topic == null) { + return; + } + + subscribeToWebSub(topic); + // 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. @@ -25,4 +53,61 @@ public class WebSubSubscriber { // - W3C WebSub Recommendation: https://www.w3.org/TR/websub/ // - the implementation notes of the WebSub hub you are using to distribute events } + + private String discoverWebSubTopic(String endpoint) { + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(endpoint)) + .header("Content-Type", "application/json") + .GET() + .build(); + + + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + if (response.statusCode() == HttpStatus.OK.value()) { + // TODO decide with other groups about response structure and replace the hardcoded + // uri with response uri + JSONObject jsonObject = new JSONObject(response.body()); + System.out.println(jsonObject); + return jsonObject.getString("topic"); + } else { + logger.log(Level.SEVERE, "Could not find a websub uri"); + } + } catch (InterruptedException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + } + return null; + } + + private void subscribeToWebSub(String topic) { + HttpClient client = HttpClient.newHttpClient(); + + String body = new JSONObject() + .put("hub.callback", AUCTION_HOUSE_ENDPOINT + "/auction-started") + .put("hub.mode", "subscribe") + .put("hub.topic", topic) + .put("hub.ws", false) + .toString(); + + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(WEBSUB_HUB_ENDPOINT)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + + try { + client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + } + } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java index d156452..9e7a356 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java @@ -1,6 +1,10 @@ package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub; import ch.unisg.tapas.auctionhouse.application.handler.AuctionStartedHandler; + +import org.json.JSONArray; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; /** @@ -13,6 +17,21 @@ public class AuctionStartedEventListenerWebSubAdapter { public AuctionStartedEventListenerWebSubAdapter(AuctionStartedHandler auctionStartedHandler) { this.auctionStartedHandler = auctionStartedHandler; } + /** + * Controller which listens to auction-started callbacks + * @return 200 OK + **/ + @PostMapping(path = "/auction-started") + public ResponseEntity handleExecutorAddedEvent(@RequestBody String payload) { - //TODO + // Payload should be a JSONArray with auctions + JSONArray jsonArray = new JSONArray(payload); + for (Object auction : jsonArray) { + System.out.println(auction); + // TODO logic to call handleAuctionStartedEvent() + // auctionStartedHandler.handleAuctionStartedEvent(auctionStartedEvent) + } + + return new ResponseEntity<>(HttpStatus.OK); + } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java new file mode 100644 index 0000000..9e25a69 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java @@ -0,0 +1,36 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub; + +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +/** + * This class validates the subscription intent from the websub hub + */ +@RestController +public class ValidateIntentWebSubAdapter { + + @Value("${application.environment}") + private String environment; + + @GetMapping(path = "/auction-started") + public ResponseEntity handleExecutorAddedEvent(@RequestParam("hub.challenge") String challenge) { + + + + // Different implementation depending on local development or production + if (environment.equalsIgnoreCase("development")) { + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Type", "application/json"); + String body = new JSONObject() + .put("hub.challenge", challenge) + .toString(); + return new ResponseEntity<>(body, headers, HttpStatus.OK); + } else { + return new ResponseEntity<>(challenge, HttpStatus.OK); + } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java index 9e6ec67..01350d3 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java @@ -4,12 +4,16 @@ 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.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.io.IOException; +import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -17,6 +21,8 @@ import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; import java.util.stream.Collectors; /** @@ -30,8 +36,38 @@ public class PublishAuctionStartedEventWebSubAdapter implements AuctionStartedEv @Autowired private ConfigProperties config; + @Value("${auctionhouse.uri}") + private String auctionHouseUri; + + @Value("${websub.hub.uri}") + private String webSubHubUri; + + Logger logger = Logger.getLogger(PublishAuctionStartedEventWebSubAdapter.class.getName()); + @Override public void publishAuctionStartedEvent(AuctionStartedEvent event) { - // TODO + HttpClient client = HttpClient.newHttpClient(); + + String body = new JSONObject() + .put("hub.url", auctionHouseUri + "/auctions") + .put("hub.mode", "publish") + .toString(); + + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(webSubHubUri)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + + try { + client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + } } } diff --git a/tapas-auction-house/src/main/resources/application.properties b/tapas-auction-house/src/main/resources/application.properties index e9c609f..96e231c 100644 --- a/tapas-auction-house/src/main/resources/application.properties +++ b/tapas-auction-house/src/main/resources/application.properties @@ -6,3 +6,7 @@ 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/ + +application.environment=development +auctionhouse.uri=http://localhost:8086 +websub.hub.uri=http://localhost:3000 From f652a9ecafbbb565a649a41c47c6eb891cdd086b Mon Sep 17 00:00:00 2001 From: rahimiankeanu Date: Fri, 12 Nov 2021 08:51:43 +0100 Subject: [PATCH 22/40] MQTT event adapter --- ...ecutorRemovedEventListenerHttpAdapter.java | 6 ++- .../mqtt/ExecutorRemovedEventListener | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListener diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java index fcf9b52..58bbb95 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java @@ -18,7 +18,8 @@ import org.springframework.web.bind.annotation.RestController; public class ExecutorRemovedEventListenerHttpAdapter { // TODO: add annotations for request method, request URI, etc. - public void handleExecutorRemovedEvent(@PathVariable("executorId") String executorId) { + @PostMapping(path = "/executors/{taskType}/{executorId}") + public ResponseEntity handleExecutorRemovedEvent(@PathVariable("executorId") String executorId) { // TODO: implement logic ExecutorRemovedEvent executorRemovedEvent = new ExecutorRemovedEvent( @@ -27,6 +28,7 @@ public class ExecutorRemovedEventListenerHttpAdapter { ExecutorRemovedHandler newExecutorHandler = new ExecutorRemovedHandler(); newExecutorHandler.handleExecutorRemovedEvent(executorRemovedEvent); - + + return new ResponseEntity<>(HttpStatus.NO_CONTENT); } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListener b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListener new file mode 100644 index 0000000..087479c --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListener @@ -0,0 +1,46 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import ch.unisg.tapas.auctionhouse.application.handler.ExecutorRemovedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEvent; +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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * Listener that handles events when an executor was removed to this TAPAS application. + * + * This class is only provided as an example to help you bootstrap the project. + */ +public class ExecutorRemovedEventListenerMqttAdapter extends AuctionEventMqttListener { + private static final Logger LOGGER = LogManager.getLogger(ExecutorRemovedEventListenerMqttAdapter.class); + + @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 executorId = data.get("executorId").asText(); + + ExecutorRemovedEvent executorRemovedEvent = new ExecutorRemovedEvent( + new ExecutorRegistry.ExecutorIdentifier(executorId) + ); + + ExecutorRemovedHandler newExecutorHandler = new ExecutorRemovedHandler(); + newExecutorHandler.handleNewExecutorEvent(executorRemovedEvent); + } catch (JsonProcessingException | NullPointerException e) { + LOGGER.error(e.getMessage(), e); + return false; + } + + return true; + } +} From c48a402e559a39182292d07c04eb8c6c142b8a34 Mon Sep 17 00:00:00 2001 From: rahimiankeanu Date: Fri, 12 Nov 2021 08:59:09 +0100 Subject: [PATCH 23/40] 2.0 --- ...ntListener => ExecutorRemovedEventListenerMqttAdapter.java} | 0 .../application/handler/ExecutorRemovedHandler.java | 3 +++ 2 files changed, 3 insertions(+) rename tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/{ExecutorRemovedEventListener => ExecutorRemovedEventListenerMqttAdapter.java} (100%) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListener b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java similarity index 100% rename from tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListener rename to tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java index c3bfed8..aa47d64 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java @@ -16,4 +16,7 @@ public class ExecutorRemovedHandler implements ExecutorRemovedEventHandler { public boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent) { return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorId()); } + + public void handleNewExecutorEvent(ExecutorRemovedEvent executorRemovedEvent) { + } } From 2f42da485d68f50e102e9329f3ac4a56a15de113 Mon Sep 17 00:00:00 2001 From: reynisson Date: Fri, 12 Nov 2021 13:30:16 +0100 Subject: [PATCH 24/40] Fixed imports so that the class builds --- .../application/port/in/LaunchAuctionCommand.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java index 37eb5db..626fa49 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java @@ -1,11 +1,10 @@ package ch.unisg.tapas.auctionhouse.application.port.in; import ch.unisg.tapas.auctionhouse.domain.Auction; -import ch.unisg.common.SelfValidating; -import lombok.NonNull; +import ch.unisg.tapas.common.SelfValidating; import lombok.Value; -import javax.validation.constraint.NotNull; +import javax.validation.constraints.NotNull; /** * Command for launching an auction in this auction house. From 55c094fc56db6abc9c641fd45448af1e8bef342b Mon Sep 17 00:00:00 2001 From: reynisson Date: Sun, 14 Nov 2021 14:28:45 +0100 Subject: [PATCH 25/40] Implemented the new executor added event over mqtt --- executor-pool/pom.xml | 6 +++ .../ch/unisg/common/ConfigProperties.java | 23 ++++++++++ .../ch/unisg/executorpool/TestController.java | 12 ------ .../common/clients/TapasMqttClient.java | 41 ++++++++++++++++++ ...ewExecutorToExecutorPoolWebController.java | 8 +++- .../PublishExecutorAddedEventAdapter.java | 43 +++++++++++++++++++ .../port/out/ExecutorAddedEventPort.java | 8 ++++ .../AddNewExecutorToExecutorPoolService.java | 17 ++++++-- .../domain/ExecutorAddedEvent.java | 10 +++++ .../src/main/resources/application.properties | 2 + 10 files changed, 153 insertions(+), 17 deletions(-) create mode 100644 executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java delete mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/TestController.java create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/clients/TapasMqttClient.java create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorAddedEventAdapter.java create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorAddedEventPort.java create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorAddedEvent.java diff --git a/executor-pool/pom.xml b/executor-pool/pom.xml index 2e75dcc..512235d 100644 --- a/executor-pool/pom.xml +++ b/executor-pool/pom.xml @@ -63,6 +63,12 @@ javax.transaction-api compile + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + compile + diff --git a/executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java b/executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java new file mode 100644 index 0000000..b46bf63 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java @@ -0,0 +1,23 @@ +package ch.unisg.common; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@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 getMqttBrokerUri() { + return URI.create(environment.getProperty("mqtt.broker.uri")); + } +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/TestController.java b/executor-pool/src/main/java/ch/unisg/executorpool/TestController.java deleted file mode 100644 index ca29e09..0000000 --- a/executor-pool/src/main/java/ch/unisg/executorpool/TestController.java +++ /dev/null @@ -1,12 +0,0 @@ -package ch.unisg.executorpool; - -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class TestController { - @RequestMapping("/") - public String index() { - return "Hello World! Executor Pool"; - } -} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/clients/TapasMqttClient.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/clients/TapasMqttClient.java new file mode 100644 index 0000000..0b24b81 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/clients/TapasMqttClient.java @@ -0,0 +1,41 @@ +package ch.unisg.executorpool.adapter.common.clients; + +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; + +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 TapasMqttClient(String brokerAddress) { + this.mqttClientId = UUID.randomUUID().toString(); + this.brokerAddress = brokerAddress; + } + + public static synchronized TapasMqttClient getInstance(String brokerAddress) { + + if (tapasClient == null) { + tapasClient = new TapasMqttClient(brokerAddress); + } + + return tapasClient; + } + + public void publishMessage(String topic, String payload) throws MqttException { + mqttClient = new org.eclipse.paho.client.mqttv3.MqttClient(brokerAddress, mqttClientId, new MemoryPersistence()); + mqttClient.connect(); + MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); + mqttClient.publish(topic, message); + mqttClient.disconnect(); + } +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java index 5a2dc09..ff464d3 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java @@ -1,5 +1,6 @@ package ch.unisg.executorpool.adapter.in.web; +import ch.unisg.executorpool.adapter.common.clients.TapasMqttClient; import ch.unisg.executorpool.adapter.common.formats.ExecutorJsonRepresentation; import ch.unisg.executorpool.application.port.in.AddNewExecutorToExecutorPoolUseCase; import ch.unisg.executorpool.application.port.in.AddNewExecutorToExecutorPoolCommand; @@ -13,6 +14,10 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import javax.validation.ConstraintViolationException; import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +import org.eclipse.paho.client.mqttv3.*; @RestController public class AddNewExecutorToExecutorPoolWebController { @@ -24,7 +29,7 @@ public class AddNewExecutorToExecutorPoolWebController { @PostMapping(path = "/executor-pool/AddExecutor", consumes = {ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE}) public ResponseEntity addNewExecutorToExecutorPool(@RequestBody ExecutorJsonRepresentation payload){ - try{ + try { AddNewExecutorToExecutorPoolCommand command = new AddNewExecutorToExecutorPoolCommand( new ExecutorClass.ExecutorUri(URI.create(payload.getExecutorUri())), new ExecutorClass.ExecutorTaskType(payload.getExecutorTaskType()) @@ -36,6 +41,7 @@ public class AddNewExecutorToExecutorPoolWebController { responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE); return new ResponseEntity<>(ExecutorJsonRepresentation.serialize(newExecutor), responseHeaders, HttpStatus.CREATED); + } catch (ConstraintViolationException e){ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorAddedEventAdapter.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorAddedEventAdapter.java new file mode 100644 index 0000000..323bcbb --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorAddedEventAdapter.java @@ -0,0 +1,43 @@ +package ch.unisg.executorpool.adapter.out.messaging; + +import ch.unisg.common.ConfigProperties; +import ch.unisg.executorpool.adapter.common.clients.TapasMqttClient; +import ch.unisg.executorpool.adapter.common.formats.ExecutorJsonRepresentation; +import ch.unisg.executorpool.application.port.out.ExecutorAddedEventPort; +import ch.unisg.executorpool.domain.ExecutorAddedEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import java.net.URI; + +@Component +@Primary +public class PublishExecutorAddedEventAdapter implements ExecutorAddedEventPort { + + private static final Logger LOGGER = LogManager.getLogger(PublishExecutorAddedEventAdapter.class); + + // TODO Can't autowire. Find fix + /* + @Autowired + private ConfigProperties config; + */ + + @Autowired + private Environment environment; + + @Override + public void publishExecutorAddedEvent(ExecutorAddedEvent event){ + try{ + var mqttClient = TapasMqttClient.getInstance(environment.getProperty("mqtt.broker.uri")); + mqttClient.publishMessage("ch/unisg/tapas/executors/added", ExecutorJsonRepresentation.serialize(event.getExecutorClass())); + } + catch (MqttException e){ + LOGGER.error(e.getMessage(), e); + } + } +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorAddedEventPort.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorAddedEventPort.java new file mode 100644 index 0000000..ad75c75 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorAddedEventPort.java @@ -0,0 +1,8 @@ +package ch.unisg.executorpool.application.port.out; + +import ch.unisg.executorpool.domain.ExecutorAddedEvent; +import org.eclipse.paho.client.mqttv3.MqttException; + +public interface ExecutorAddedEventPort { + void publishExecutorAddedEvent(ExecutorAddedEvent event); +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java index 200739b..393024a 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java @@ -2,24 +2,33 @@ package ch.unisg.executorpool.application.service; import ch.unisg.executorpool.application.port.in.AddNewExecutorToExecutorPoolUseCase; import ch.unisg.executorpool.application.port.in.AddNewExecutorToExecutorPoolCommand; +import ch.unisg.executorpool.application.port.out.ExecutorAddedEventPort; +import ch.unisg.executorpool.domain.ExecutorAddedEvent; import ch.unisg.executorpool.domain.ExecutorClass; import ch.unisg.executorpool.domain.ExecutorPool; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; -import org.yaml.snakeyaml.constructor.DuplicateKeyException; import javax.transaction.Transactional; -import javax.validation.ConstraintViolationException; -@RequiredArgsConstructor @Component @Transactional public class AddNewExecutorToExecutorPoolService implements AddNewExecutorToExecutorPoolUseCase { + private final ExecutorAddedEventPort executorAddedEventPort; + + public AddNewExecutorToExecutorPoolService(ExecutorAddedEventPort executorAddedEventPort){ + this.executorAddedEventPort = executorAddedEventPort; + } + @Override public ExecutorClass addNewExecutorToExecutorPool(AddNewExecutorToExecutorPoolCommand command){ ExecutorPool executorPool = ExecutorPool.getExecutorPool(); + var newExecutor = executorPool.addNewExecutor(command.getExecutorUri(), command.getExecutorTaskType()); - return executorPool.addNewExecutor(command.getExecutorUri(), command.getExecutorTaskType()); + var executorAddedEvent = new ExecutorAddedEvent(newExecutor); + executorAddedEventPort.publishExecutorAddedEvent(executorAddedEvent); + + return newExecutor; } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorAddedEvent.java b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorAddedEvent.java new file mode 100644 index 0000000..6ec291e --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorAddedEvent.java @@ -0,0 +1,10 @@ +package ch.unisg.executorpool.domain; + +import lombok.Getter; + +public class ExecutorAddedEvent { + @Getter + private ExecutorClass executorClass; + + public ExecutorAddedEvent(ExecutorClass executorClass) { this.executorClass = executorClass; } +} diff --git a/executor-pool/src/main/resources/application.properties b/executor-pool/src/main/resources/application.properties index 8f91ca7..0c9ba7e 100644 --- a/executor-pool/src/main/resources/application.properties +++ b/executor-pool/src/main/resources/application.properties @@ -1 +1,3 @@ server.port=8083 + +mqtt.broker.uri=tcp://localhost:1883 From 2999fb294c32f334943c127fd764201361468260 Mon Sep 17 00:00:00 2001 From: reynisson Date: Sun, 14 Nov 2021 15:16:42 +0100 Subject: [PATCH 26/40] Implemented the auction started event over mqtt --- .../ch/unisg/common/ConfigProperties.java | 5 ++- .../tapas/TapasAuctionHouseApplication.java | 4 +-- .../common/clients/TapasMqttClient.java | 5 ++- .../mqtt/AuctionEventsMqttDispatcher.java | 2 +- ...PublishAuctionStartedEventMqttAdapter.java | 36 +++++++++++++++++++ ...blishAuctionStartedEventWebSubAdapter.java | 1 - .../unisg/tapas/common/ConfigProperties.java | 10 ++++++ .../src/main/resources/application.properties | 3 ++ 8 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventMqttAdapter.java diff --git a/executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java b/executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java index b46bf63..253922c 100644 --- a/executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java +++ b/executor-pool/src/main/java/ch/unisg/common/ConfigProperties.java @@ -12,10 +12,9 @@ public class ConfigProperties { private Environment environment; /** - * Retrieves the URI of the WebSub hub. In this project, we use a single WebSub hub, but we could - * use multiple. + * Retrieves the URI of the MQTT broker. * - * @return the URI of the WebSub hub + * @return the URI of the MQTT broker */ public URI getMqttBrokerUri() { return URI.create(environment.getProperty("mqtt.broker.uri")); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java index 8fc22d0..db57cc7 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -21,14 +21,14 @@ 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 String MQTT_BROKER = "tcp://localhost:1883"; public static void main(String[] args) { SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class); // We will use these bootstrap methods in Week 6: // bootstrapMarketplaceWithWebSub(); - // bootstrapMarketplaceWithMqtt(); + bootstrapMarketplaceWithMqtt(); tapasAuctioneerApp.run(args); } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java index 708d512..db5903c 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java @@ -68,7 +68,10 @@ public class TapasMqttClient { mqttClient.subscribe(topic); } - private void publishMessage(String topic, String payload) throws MqttException { + public void publishMessage(String topic, String payload) throws MqttException { + mqttClient = new org.eclipse.paho.client.mqttv3.MqttClient(brokerAddress, mqttClientId, new MemoryPersistence()); + mqttClient.connect(); + MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); mqttClient.publish(topic, message); } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java index e5eaf12..3e55d5e 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java @@ -26,7 +26,7 @@ public class AuctionEventsMqttDispatcher { // TODO: Register here your topics and event listener adapters private void initRouter() { - router.put("ch/unisg/tapas-group-tutors/executors", new ExecutorAddedEventListenerMqttAdapter()); + router.put("ch/unisg/tapas/executors/added", new ExecutorAddedEventListenerMqttAdapter()); } /** diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventMqttAdapter.java new file mode 100644 index 0000000..d5bb0fc --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventMqttAdapter.java @@ -0,0 +1,36 @@ +package ch.unisg.tapas.auctionhouse.adapter.out.messaging.websub; + +import ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; +import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher; +import ch.unisg.tapas.auctionhouse.application.port.out.AuctionStartedEventPort; +import ch.unisg.tapas.auctionhouse.domain.AuctionStartedEvent; +import ch.unisg.tapas.common.ConfigProperties; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +@Component +@Primary +public class PublishAuctionStartedEventMqttAdapter implements AuctionStartedEventPort { + + private static final Logger LOGGER = LogManager.getLogger(PublishAuctionStartedEventMqttAdapter.class); + + @Autowired + private ConfigProperties config; + + @Override + public void publishAuctionStartedEvent(AuctionStartedEvent event) { + try{ + var mqttClient = TapasMqttClient.getInstance(config.getMqttBrokerUri().toString(), new AuctionEventsMqttDispatcher()); + mqttClient.publishMessage("ch/unisg/tapas/auctions", AuctionJsonRepresentation.serialize(event.getAuction())); + } + catch (MqttException | JsonProcessingException e){ + LOGGER.error(e.getMessage(), e); + } + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java index 9e6ec67..73451e4 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventWebSubAdapter.java @@ -23,7 +23,6 @@ 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. diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java index 748afda..2933465 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/common/ConfigProperties.java @@ -61,4 +61,14 @@ public class ConfigProperties { public URI getTaskListUri() { return URI.create(environment.getProperty("tasks.list.uri")); } + + + /** + * Retrieves the URI of the MQTT broker. + * + * @return the URI of the MQTT broker + */ + public URI getMqttBrokerUri() { + return URI.create(environment.getProperty("mqtt.broker.uri")); + } } diff --git a/tapas-auction-house/src/main/resources/application.properties b/tapas-auction-house/src/main/resources/application.properties index e9c609f..1ededee 100644 --- a/tapas-auction-house/src/main/resources/application.properties +++ b/tapas-auction-house/src/main/resources/application.properties @@ -6,3 +6,6 @@ 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/ + + +mqtt.broker.uri=tcp://localhost:1883 From 41b0e25a5e985bef5a174d0484ff603535ce1047 Mon Sep 17 00:00:00 2001 From: reynisson Date: Sun, 14 Nov 2021 16:06:03 +0100 Subject: [PATCH 27/40] Adapted the auction house to receive and handle the new executor event over mqtt --- .../tapas/TapasAuctionHouseApplication.java | 5 +++ .../common/clients/TapasMqttClient.java | 2 +- ...ExecutorAddedEventListenerHttpAdapter.java | 34 ------------------- ...ecutorRemovedEventListenerHttpAdapter.java | 16 --------- ...ExecutorAddedEventListenerMqttAdapter.java | 12 ++++--- .../handler/ExecutorAddedHandler.java | 2 +- .../handler/ExecutorRemovedHandler.java | 2 +- .../port/in/ExecutorAddedEvent.java | 11 +++--- .../port/in/ExecutorRemovedEvent.java | 11 +++--- .../auctionhouse/domain/ExecutorRegistry.java | 23 +++++++------ 10 files changed, 39 insertions(+), 79 deletions(-) delete mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java delete mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java index db57cc7..46dafb7 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -4,9 +4,11 @@ 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 ch.unisg.tapas.common.ConfigProperties; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -20,6 +22,9 @@ import java.util.List; public class TapasAuctionHouseApplication { private static final Logger LOGGER = LogManager.getLogger(TapasAuctionHouseApplication.class); + @Autowired + private ConfigProperties config; + public static String RESOURCE_DIRECTORY = "https://api.interactions.ics.unisg.ch/auction-houses/"; public static String MQTT_BROKER = "tcp://localhost:1883"; diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java index db5903c..1a30bc4 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/TapasMqttClient.java @@ -71,7 +71,7 @@ public class TapasMqttClient { public void publishMessage(String topic, String payload) throws MqttException { mqttClient = new org.eclipse.paho.client.mqttv3.MqttClient(brokerAddress, mqttClientId, new MemoryPersistence()); mqttClient.connect(); - + MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); mqttClient.publish(topic, message); } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java deleted file mode 100644 index 3511b7d..0000000 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorAddedEventListenerHttpAdapter.java +++ /dev/null @@ -1,34 +0,0 @@ -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; - -/** - * Template for receiving an executor added event via HTTP - */ -@RestController -public class ExecutorAddedEventListenerHttpAdapter { - - @PostMapping(path = "/executors/{taskType}/{executorId}") - public ResponseEntity handleExecutorAddedEvent(@PathVariable("taskType") String taskType, - @PathVariable("executorId") String executorId) { - - ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( - new ExecutorRegistry.ExecutorIdentifier(executorId), - new Auction.AuctionedTaskType(taskType) - ); - - ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler(); - newExecutorHandler.handleNewExecutorEvent(executorAddedEvent); - - return new ResponseEntity<>(HttpStatus.NO_CONTENT); - } -} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java deleted file mode 100644 index 53811f9..0000000 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java +++ /dev/null @@ -1,16 +0,0 @@ -package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http; - -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RestController; - -/** - * Template for handling an executor removed event received via an HTTP request - */ -@RestController -public class ExecutorRemovedEventListenerHttpAdapter { - - // TODO: add annotations for request method, request URI, etc. - public void handleExecutorRemovedEvent(@PathVariable("executorId") String executorId) { - // TODO: implement logic - } -} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java index 2f661d1..dd2d120 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java @@ -11,6 +11,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.MqttMessage; +import java.net.URI; + /** * Listener that handles events when an executor was added to this TAPAS application. * @@ -24,16 +26,16 @@ public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListe String payload = new String(message.getPayload()); try { - // Note: this messge representation is provided only as an example. You should use a + // Note: this message 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(); + String executorUri = data.get("executorUri").asText(); + String executorTaskType = data.get("executorTaskType").asText(); ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( - new ExecutorRegistry.ExecutorIdentifier(executorId), - new Auction.AuctionedTaskType(taskType) + new ExecutorRegistry.ExecutorUri(URI.create(executorUri)), + new Auction.AuctionedTaskType(executorTaskType) ); ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler(); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java index 624e669..fc30e11 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorAddedHandler.java @@ -11,6 +11,6 @@ public class ExecutorAddedHandler implements ExecutorAddedEventHandler { @Override public boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent) { return ExecutorRegistry.getInstance().addExecutor(executorAddedEvent.getTaskType(), - executorAddedEvent.getExecutorId()); + executorAddedEvent.getExecutorUri()); } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java index c3bfed8..9a68da1 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java @@ -14,6 +14,6 @@ public class ExecutorRemovedHandler implements ExecutorRemovedEventHandler { @Override public boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent) { - return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorId()); + return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorUri()); } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java index 5a53b94..7d647e1 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorAddedEvent.java @@ -1,7 +1,8 @@ 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.auctionhouse.domain.ExecutorRegistry; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorUri; import ch.unisg.tapas.common.SelfValidating; import lombok.Value; @@ -13,7 +14,7 @@ import javax.validation.constraints.NotNull; @Value public class ExecutorAddedEvent extends SelfValidating { @NotNull - private final ExecutorIdentifier executorId; + private final ExecutorRegistry.ExecutorUri executorUri; @NotNull private final AuctionedTaskType taskType; @@ -21,10 +22,10 @@ public class ExecutorAddedEvent extends SelfValidating { /** * Constructs an executor added event. * - * @param executorId the identifier of the executor that was added to this TAPAS application + * @param executorUri the identifier of the executor that was added to this TAPAS application */ - public ExecutorAddedEvent(ExecutorIdentifier executorId, AuctionedTaskType taskType) { - this.executorId = executorId; + public ExecutorAddedEvent(ExecutorUri executorUri, AuctionedTaskType taskType) { + this.executorUri = executorUri; this.taskType = taskType; this.validateSelf(); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java index 4d5c910..a1633fe 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/ExecutorRemovedEvent.java @@ -1,6 +1,7 @@ package ch.unisg.tapas.auctionhouse.application.port.in; -import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorIdentifier; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry; +import ch.unisg.tapas.auctionhouse.domain.ExecutorRegistry.ExecutorUri; import ch.unisg.tapas.common.SelfValidating; import lombok.Value; @@ -12,15 +13,15 @@ import javax.validation.constraints.NotNull; @Value public class ExecutorRemovedEvent extends SelfValidating { @NotNull - private final ExecutorIdentifier executorId; + private final ExecutorUri executorUri; /** * Constructs an executor removed event. * - * @param executorId the identifier of the executor that was removed from this TAPAS application + * @param executorUri */ - public ExecutorRemovedEvent(ExecutorIdentifier executorId) { - this.executorId = executorId; + public ExecutorRemovedEvent(ExecutorUri executorUri) { + this.executorUri = executorUri; this.validateSelf(); } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java index 9da3756..1aedc80 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/ExecutorRegistry.java @@ -2,6 +2,7 @@ package ch.unisg.tapas.auctionhouse.domain; import lombok.Value; +import java.net.URI; import java.util.*; /** @@ -13,7 +14,7 @@ import java.util.*; public class ExecutorRegistry { private static ExecutorRegistry registry; - private final Map> executors; + private final Map> executors; private ExecutorRegistry() { this.executors = new Hashtable<>(); @@ -31,14 +32,14 @@ public class ExecutorRegistry { * 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) + * @param executorUri the executor's URI * @return true unless a runtime exception occurs */ - public boolean addExecutor(Auction.AuctionedTaskType taskType, ExecutorIdentifier executorIdentifier) { - Set taskTypeExecs = executors.getOrDefault(taskType, + public boolean addExecutor(Auction.AuctionedTaskType taskType, ExecutorUri executorUri) { + Set taskTypeExecs = executors.getOrDefault(taskType, Collections.synchronizedSet(new HashSet<>())); - taskTypeExecs.add(executorIdentifier); + taskTypeExecs.add(executorUri); executors.put(taskType, taskTypeExecs); return true; @@ -47,17 +48,17 @@ public class ExecutorRegistry { /** * 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) + * @param executorUri the executor's URI * @return true unless a runtime exception occurs */ - public boolean removeExecutor(ExecutorIdentifier executorIdentifier) { + public boolean removeExecutor(ExecutorUri executorUri) { Iterator iterator = executors.keySet().iterator(); while (iterator.hasNext()) { Auction.AuctionedTaskType taskType = iterator.next(); - Set set = executors.get(taskType); + Set set = executors.get(taskType); - set.remove(executorIdentifier); + set.remove(executorUri); if (set.isEmpty()) { iterator.remove(); @@ -80,7 +81,7 @@ public class ExecutorRegistry { // Value Object for the executor identifier @Value - public static class ExecutorIdentifier { - String value; + public static class ExecutorUri { + URI value; } } From 396f24e0076875933bdab96fa4c34fd7b055f552 Mon Sep 17 00:00:00 2001 From: reynisson Date: Sun, 14 Nov 2021 16:34:45 +0100 Subject: [PATCH 28/40] Implemented the executor removed event over mqtt --- .../PublishExecutorRemovedEventAdapter.java | 41 +++++++++++++++++++ .../port/out/ExecutorRemovedEventPort.java | 7 ++++ ...RemoveExecutorFromExecutorPoolService.java | 19 ++++++++- .../domain/ExecutorRemovedEvent.java | 11 +++++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorRemovedEventAdapter.java create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorRemovedEventPort.java create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorRemovedEvent.java diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorRemovedEventAdapter.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorRemovedEventAdapter.java new file mode 100644 index 0000000..aa01165 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/out/messaging/PublishExecutorRemovedEventAdapter.java @@ -0,0 +1,41 @@ +package ch.unisg.executorpool.adapter.out.messaging; + +import ch.unisg.executorpool.adapter.common.clients.TapasMqttClient; +import ch.unisg.executorpool.adapter.common.formats.ExecutorJsonRepresentation; +import ch.unisg.executorpool.application.port.out.ExecutorRemovedEventPort; +import ch.unisg.executorpool.domain.ExecutorAddedEvent; +import ch.unisg.executorpool.domain.ExecutorRemovedEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Primary; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +@Primary +public class PublishExecutorRemovedEventAdapter implements ExecutorRemovedEventPort { + + private static final Logger LOGGER = LogManager.getLogger(PublishExecutorAddedEventAdapter.class); + + // TODO Can't autowire. Find fix + /* + @Autowired + private ConfigProperties config; + */ + + @Autowired + private Environment environment; + + @Override + public void publishExecutorRemovedEvent(ExecutorRemovedEvent event){ + try{ + var mqttClient = TapasMqttClient.getInstance(environment.getProperty("mqtt.broker.uri")); + mqttClient.publishMessage("ch/unisg/tapas/executors/removed", ExecutorJsonRepresentation.serialize(event.getExecutorClass())); + } + catch (MqttException e){ + LOGGER.error(e.getMessage(), e); + } + } +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorRemovedEventPort.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorRemovedEventPort.java new file mode 100644 index 0000000..b905858 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/out/ExecutorRemovedEventPort.java @@ -0,0 +1,7 @@ +package ch.unisg.executorpool.application.port.out; + +import ch.unisg.executorpool.domain.ExecutorRemovedEvent; + +public interface ExecutorRemovedEventPort { + void publishExecutorRemovedEvent(ExecutorRemovedEvent event); +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java index a606f57..4d2457d 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java @@ -2,21 +2,36 @@ package ch.unisg.executorpool.application.service; import ch.unisg.executorpool.application.port.in.RemoveExecutorFromExecutorPoolCommand; import ch.unisg.executorpool.application.port.in.RemoveExecutorFromExecutorPoolUseCase; +import ch.unisg.executorpool.application.port.out.ExecutorRemovedEventPort; import ch.unisg.executorpool.domain.ExecutorClass; import ch.unisg.executorpool.domain.ExecutorPool; +import ch.unisg.executorpool.domain.ExecutorRemovedEvent; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import javax.transaction.Transactional; import java.util.Optional; -@RequiredArgsConstructor @Component @Transactional public class RemoveExecutorFromExecutorPoolService implements RemoveExecutorFromExecutorPoolUseCase { + + private final ExecutorRemovedEventPort executorRemovedEventPort; + + public RemoveExecutorFromExecutorPoolService(ExecutorRemovedEventPort executorRemovedEventPort){ + this.executorRemovedEventPort = executorRemovedEventPort; + } + @Override public Optional removeExecutorFromExecutorPool(RemoveExecutorFromExecutorPoolCommand command){ ExecutorPool executorPool = ExecutorPool.getExecutorPool(); - return executorPool.removeExecutorByIpAndPort(command.getExecutorUri()); + var removedExecutor = executorPool.removeExecutorByIpAndPort(command.getExecutorUri()); + + if(removedExecutor.isPresent()){ + var executorRemovedEvent = new ExecutorRemovedEvent(removedExecutor.get()); + executorRemovedEventPort.publishExecutorRemovedEvent(executorRemovedEvent); + } + + return removedExecutor; } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorRemovedEvent.java b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorRemovedEvent.java new file mode 100644 index 0000000..a038928 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorRemovedEvent.java @@ -0,0 +1,11 @@ +package ch.unisg.executorpool.domain; + +import ch.unisg.executorpool.domain.ExecutorClass; +import lombok.Getter; + +public class ExecutorRemovedEvent { + @Getter + private ExecutorClass executorClass; + + public ExecutorRemovedEvent(ExecutorClass executorClass) { this.executorClass = executorClass; } +} From b37141f5cefcdcfef0c4b17778670f090b5e259e Mon Sep 17 00:00:00 2001 From: "julius.lautz" Date: Sun, 14 Nov 2021 16:51:42 +0100 Subject: [PATCH 29/40] fixed issues --- .../adapter/common/formats/AuctionJsonRepresentation.java | 6 ++++-- .../application/service/StartAuctionService.java | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java index 4500423..ea4cf2c 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java @@ -7,6 +7,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Getter; import lombok.Setter; +import java.sql.Timestamp; + /** * 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 @@ -28,12 +30,12 @@ public class AuctionJsonRepresentation { private String taskType; @Getter @Setter - private Integer deadline; + private Timestamp deadline; public AuctionJsonRepresentation() { } public AuctionJsonRepresentation(String auctionId, String auctionHouseUri, String taskUri, - String taskType, Integer deadline) { + String taskType, Timestamp deadline) { this.auctionId = auctionId; this.auctionHouseUri = auctionHouseUri; this.taskUri = taskUri; diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java index b9d9d3d..60c5f24 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java @@ -12,6 +12,7 @@ import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.sql.Timestamp; import java.util.Optional; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -26,7 +27,7 @@ import java.util.concurrent.TimeUnit; public class StartAuctionService implements LaunchAuctionUseCase { private static final Logger LOGGER = LogManager.getLogger(StartAuctionService.class); - private final static int DEFAULT_AUCTION_DEADLINE_MILLIS = 10000; + private final Timestamp DEFAULT_AUCTION_DEADLINE_MILLIS = Timestamp.valueOf("1970-01-01 00:00:01"); // Event port used to publish an auction started event private final AuctionStartedEventPort auctionStartedEventPort; From 1b7395ba0d04f244b530978bd27edd558ff47e5a Mon Sep 17 00:00:00 2001 From: reynisson Date: Sun, 14 Nov 2021 19:26:19 +0100 Subject: [PATCH 30/40] Implemented the handling of the mqtt executor removed event --- .../in/messaging/mqtt/AuctionEventsMqttDispatcher.java | 1 + .../mqtt/ExecutorRemovedEventListenerMqttAdapter.java | 8 +++++--- .../application/handler/ExecutorRemovedHandler.java | 3 --- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java index 3e55d5e..7d30453 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java @@ -27,6 +27,7 @@ public class AuctionEventsMqttDispatcher { // TODO: Register here your topics and event listener adapters private void initRouter() { router.put("ch/unisg/tapas/executors/added", new ExecutorAddedEventListenerMqttAdapter()); + router.put("ch/unisg/tapas/executors/removed", new ExecutorRemovedEventListenerMqttAdapter()); } /** diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java index 087479c..4f4db7a 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java @@ -11,6 +11,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.MqttMessage; +import java.net.URI; + /** * Listener that handles events when an executor was removed to this TAPAS application. * @@ -28,14 +30,14 @@ public class ExecutorRemovedEventListenerMqttAdapter extends AuctionEventMqttLis // representation that makes sense in the context of your application. JsonNode data = new ObjectMapper().readTree(payload); - String executorId = data.get("executorId").asText(); + String executorUri = data.get("executorUri").asText(); ExecutorRemovedEvent executorRemovedEvent = new ExecutorRemovedEvent( - new ExecutorRegistry.ExecutorIdentifier(executorId) + new ExecutorRegistry.ExecutorUri(URI.create(executorUri)) ); ExecutorRemovedHandler newExecutorHandler = new ExecutorRemovedHandler(); - newExecutorHandler.handleNewExecutorEvent(executorRemovedEvent); + newExecutorHandler.handleExecutorRemovedEvent(executorRemovedEvent); } catch (JsonProcessingException | NullPointerException e) { LOGGER.error(e.getMessage(), e); return false; diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java index f63950d..9a68da1 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/ExecutorRemovedHandler.java @@ -16,7 +16,4 @@ public class ExecutorRemovedHandler implements ExecutorRemovedEventHandler { public boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent) { return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorUri()); } - - public void handleNewExecutorEvent(ExecutorRemovedEvent executorRemovedEvent) { - } } From 333f6aab2117f652f5404d25371446269ce4a38c Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 14 Nov 2021 20:46:42 +0100 Subject: [PATCH 31/40] finished websub implementation --- .../tapas/TapasAuctionHouseApplication.java | 12 ++------ .../common/clients/WebSubSubscriber.java | 4 +-- ...tionStartedEventListenerWebSubAdapter.java | 30 ++++++++++++++----- .../websub/ValidateIntentWebSubAdapter.java | 3 -- 4 files changed, 27 insertions(+), 22 deletions(-) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java index 3459cff..1f958d9 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -27,23 +27,17 @@ public class TapasAuctionHouseApplication { 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); - - // We will use these bootstrap methods in Week 6: - - // bootstrapMarketplaceWithMqtt(); - bootstrapMarketplaceWithWebSub(); } - /** * Discovers auction houses and subscribes to WebSub notifications */ private static void bootstrapMarketplaceWithWebSub() { - System.out.println("HAHA"); List auctionHouseEndpoints = discoverAuctionHouseEndpoints(); - LOGGER.info("Found auction house endpoints: " + auctionHouseEndpoints); WebSubSubscriber subscriber = new WebSubSubscriber(); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java index 8066010..5b3fc32 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/clients/WebSubSubscriber.java @@ -9,10 +9,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Component; /** * Subscribes to the WebSub hubs of auction houses discovered at run time. This class is instantiated @@ -38,6 +35,7 @@ public class WebSubSubscriber { subscribeToWebSub(topic); + // Shoudl be done :D // 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. diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java index 9e7a356..4f67dad 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/AuctionStartedEventListenerWebSubAdapter.java @@ -1,6 +1,18 @@ package ch.unisg.tapas.auctionhouse.adapter.in.messaging.websub; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; import ch.unisg.tapas.auctionhouse.application.handler.AuctionStartedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.AuctionStartedEvent; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionDeadline; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionHouseUri; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionId; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskType; +import ch.unisg.tapas.auctionhouse.domain.Auction.AuctionedTaskUri; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; import org.json.JSONArray; import org.springframework.http.HttpStatus; @@ -20,16 +32,20 @@ public class AuctionStartedEventListenerWebSubAdapter { /** * Controller which listens to auction-started callbacks * @return 200 OK + * @throws URISyntaxException **/ @PostMapping(path = "/auction-started") - public ResponseEntity handleExecutorAddedEvent(@RequestBody String payload) { + public ResponseEntity handleExecutorAddedEvent(@RequestBody Collection payload) throws URISyntaxException { - // Payload should be a JSONArray with auctions - JSONArray jsonArray = new JSONArray(payload); - for (Object auction : jsonArray) { - System.out.println(auction); - // TODO logic to call handleAuctionStartedEvent() - // auctionStartedHandler.handleAuctionStartedEvent(auctionStartedEvent) + for (AuctionJsonRepresentation auction : payload) { + auctionStartedHandler.handleAuctionStartedEvent( + new AuctionStartedEvent( + new Auction(new AuctionId(auction.getAuctionId()), + new AuctionHouseUri(new URI(auction.getAuctionHouseUri())), + new AuctionedTaskUri(new URI(auction.getTaskUri())), + new AuctionedTaskType(auction.getTaskType()), + new AuctionDeadline(auction.getDeadline())) + )); } return new ResponseEntity<>(HttpStatus.OK); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java index 9e25a69..8509b09 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java @@ -18,9 +18,6 @@ public class ValidateIntentWebSubAdapter { @GetMapping(path = "/auction-started") public ResponseEntity handleExecutorAddedEvent(@RequestParam("hub.challenge") String challenge) { - - - // Different implementation depending on local development or production if (environment.equalsIgnoreCase("development")) { HttpHeaders headers = new HttpHeaders(); From 613c1482d7365408e0512e0d3f2ff39a4a481dad Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 14 Nov 2021 22:09:10 +0100 Subject: [PATCH 32/40] fixed nameing --- .../in/messaging/websub/ValidateIntentWebSubAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java index 8509b09..7bfb450 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java @@ -17,7 +17,7 @@ public class ValidateIntentWebSubAdapter { private String environment; @GetMapping(path = "/auction-started") - public ResponseEntity handleExecutorAddedEvent(@RequestParam("hub.challenge") String challenge) { + public ResponseEntity validateIntent(@RequestParam("hub.challenge") String challenge) { // Different implementation depending on local development or production if (environment.equalsIgnoreCase("development")) { HttpHeaders headers = new HttpHeaders(); From 75feb5c4ae67566bada789f27a2df2e2cb641b11 Mon Sep 17 00:00:00 2001 From: reynisson Date: Sun, 14 Nov 2021 23:33:23 +0100 Subject: [PATCH 33/40] Renaming and small refactoring --- .../common/clients/TapasMqttClient.java | 8 ++++---- .../ExecutorAddedEventListenerMqttAdapter.java | 4 ++-- ...ner.java => ExecutorEventMqttListener.java} | 2 +- ....java => ExecutorEventsMqttDispatcher.java} | 18 ++++-------------- ...xecutorRemovedEventListenerMqttAdapter.java | 2 +- 5 files changed, 12 insertions(+), 22 deletions(-) rename roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/{AuctionEventMqttListener.java => ExecutorEventMqttListener.java} (82%) rename roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/{AuctionEventsMqttDispatcher.java => ExecutorEventsMqttDispatcher.java} (61%) diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java index 8b5411b..78f2d0c 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java @@ -1,6 +1,6 @@ package ch.unisg.roster.roster.adapter.common.clients; -import ch.unisg.roster.roster.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher; +import ch.unisg.roster.roster.adapter.in.messaging.mqtt.ExecutorEventsMqttDispatcher; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.eclipse.paho.client.mqttv3.*; @@ -25,9 +25,9 @@ public class TapasMqttClient { private final MessageReceivedCallback messageReceivedCallback; - private final AuctionEventsMqttDispatcher dispatcher; + private final ExecutorEventsMqttDispatcher dispatcher; - private TapasMqttClient(String brokerAddress, AuctionEventsMqttDispatcher dispatcher) { + private TapasMqttClient(String brokerAddress, ExecutorEventsMqttDispatcher dispatcher) { this.mqttClientId = UUID.randomUUID().toString(); this.brokerAddress = brokerAddress; @@ -37,7 +37,7 @@ public class TapasMqttClient { } public static synchronized TapasMqttClient getInstance(String brokerAddress, - AuctionEventsMqttDispatcher dispatcher) { + ExecutorEventsMqttDispatcher dispatcher) { if (tapasClient == null) { tapasClient = new TapasMqttClient(brokerAddress, dispatcher); diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java index dd9257e..1c3cbcd 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java @@ -12,7 +12,7 @@ import ch.unisg.roster.roster.application.handler.ExecutorAddedHandler; import ch.unisg.roster.roster.application.port.in.ExecutorAddedEvent; import ch.unisg.roster.roster.domain.valueobject.ExecutorType; -public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListener { +public class ExecutorAddedEventListenerMqttAdapter extends ExecutorEventMqttListener { private static final Logger LOGGER = LogManager.getLogger(ExecutorAddedEventListenerMqttAdapter.class); @Override @@ -24,7 +24,7 @@ public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListe // representation that makes sense in the context of your application. JsonNode data = new ObjectMapper().readTree(payload); - String taskType = data.get("taskType").asText(); + String taskType = data.get("executorTaskType").asText(); String executorId = data.get("executorURI").asText(); ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventMqttListener.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventMqttListener.java similarity index 82% rename from roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventMqttListener.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventMqttListener.java index 6eb109f..df46b00 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventMqttListener.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventMqttListener.java @@ -5,7 +5,7 @@ import org.eclipse.paho.client.mqttv3.MqttMessage; /** * Abstract MQTT listener for auction-related events */ -public abstract class AuctionEventMqttListener { +public abstract class ExecutorEventMqttListener { public abstract boolean handleEvent(MqttMessage message); } diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventsMqttDispatcher.java similarity index 61% rename from roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventsMqttDispatcher.java index d19c803..caa6202 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventsMqttDispatcher.java @@ -6,20 +6,10 @@ import java.util.Hashtable; import java.util.Map; import java.util.Set; -/** - * Dispatches MQTT messages for known topics to associated event listeners. Used in conjunction with - * {@link ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient}. - * - * This is where you would define MQTT topics and map them to event listeners (see - * {@link AuctionEventsMqttDispatcher#initRouter()}). - * - * This class is only provided as an example to help you bootstrap the project. You are welcomed to - * change this class as you see fit. - */ -public class AuctionEventsMqttDispatcher { - private final Map router; +public class ExecutorEventsMqttDispatcher { + private final Map router; - public AuctionEventsMqttDispatcher() { + public ExecutorEventsMqttDispatcher() { this.router = new Hashtable<>(); initRouter(); } @@ -46,7 +36,7 @@ public class AuctionEventsMqttDispatcher { * @param message the received MQTT message */ public void dispatchEvent(String topic, MqttMessage message) { - AuctionEventMqttListener listener = router.get(topic); + ExecutorEventMqttListener listener = router.get(topic); listener.handleEvent(message); } } diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java index d7b5067..71af86d 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java @@ -11,7 +11,7 @@ import ch.unisg.common.valueobject.ExecutorURI; import ch.unisg.roster.roster.application.handler.ExecutorRemovedHandler; import ch.unisg.roster.roster.application.port.in.ExecutorRemovedEvent; -public class ExecutorRemovedEventListenerMqttAdapter extends AuctionEventMqttListener { +public class ExecutorRemovedEventListenerMqttAdapter extends ExecutorEventMqttListener { private static final Logger LOGGER = LogManager.getLogger(ExecutorRemovedEventListenerMqttAdapter.class); @Override From 1c4da284800a7067d9fc00003bd8e28355d5557f Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 15 Nov 2021 11:59:27 +0100 Subject: [PATCH 34/40] fixed multiple bugs & updated cicd workflows --- .deployment/docker-compose.yml | 52 +++++++++---------- .github/workflows/build-and-deploy.yml | 29 ++++++----- .github/workflows/ci.assignment.yml | 41 --------------- ...cutor2.yml => ci.executor-computation.yml} | 12 +++-- ...ci.executor1.yml => ci.executor-robot.yml} | 10 ++-- .github/workflows/ci.roster.yml | 45 ++++++++++++++++ .../in/web/TaskAvailableController.java | 6 +++ .../web/ExecutionFinishedEventAdapter.java | 11 +++- .../adapter/out/web/GetAssignmentAdapter.java | 10 ++-- .../out/web/NotifyExecutorPoolAdapter.java | 7 +-- .../executor/domain/ExecutorBase.java | 13 +++-- .../src/main/resources/application.properties | 2 +- .../executor/domain/Executor.java | 16 +++--- .../ch/unisg/roster/RosterApplication.java | 28 ++++++++++ ...ExecutorAddedEventListenerMqttAdapter.java | 5 +- .../mqtt/ExecutorEventsMqttDispatcher.java | 4 +- .../adapter/in/web/NewTaskController.java | 8 +++ .../out/web/PublishNewTaskEventAdapter.java | 28 +++++----- .../application/service/NewTaskService.java | 1 + 19 files changed, 205 insertions(+), 123 deletions(-) delete mode 100644 .github/workflows/ci.assignment.yml rename .github/workflows/{ci.executor2.yml => ci.executor-computation.yml} (75%) rename .github/workflows/{ci.executor1.yml => ci.executor-robot.yml} (77%) create mode 100644 .github/workflows/ci.roster.yml diff --git a/.deployment/docker-compose.yml b/.deployment/docker-compose.yml index 01a5a77..5a1329f 100644 --- a/.deployment/docker-compose.yml +++ b/.deployment/docker-compose.yml @@ -56,20 +56,20 @@ services: - "traefik.http.routers.tapas-auction-house.entryPoints=web,websecure" - "traefik.http.routers.tapas-auction-house.tls.certresolver=le" - assignment: + roster: image: openjdk - command: "java -jar /data/assignment-0.0.1-SNAPSHOT.jar" + command: "java -jar /data/roster-0.0.1-SNAPSHOT.jar" restart: unless-stopped volumes: - ./:/data/ labels: - "traefik.enable=true" - - "traefik.http.routers.assignment.rule=Host(`assignment.${PUB_IP}.nip.io`)" - - "traefik.http.routers.assignment.service=assignment" - - "traefik.http.services.assignment.loadbalancer.server.port=8082" - - "traefik.http.routers.assignment.tls=true" - - "traefik.http.routers.assignment.entryPoints=web,websecure" - - "traefik.http.routers.assignment.tls.certresolver=le" + - "traefik.http.routers.roster.rule=Host(`roster.${PUB_IP}.nip.io`)" + - "traefik.http.routers.roster.service=roster" + - "traefik.http.services.roster.loadbalancer.server.port=8082" + - "traefik.http.routers.roster.tls=true" + - "traefik.http.routers.roster.entryPoints=web,websecure" + - "traefik.http.routers.roster.tls.certresolver=le" executor-pool: image: openjdk @@ -86,38 +86,38 @@ services: - "traefik.http.routers.executor-pool.entryPoints=web,websecure" - "traefik.http.routers.executor-pool.tls.certresolver=le" - executor1: + executor-computation: image: openjdk - command: "java -jar /data/executor1-0.0.1-SNAPSHOT.jar" + command: "java -jar /data/executor-computation-0.0.1-SNAPSHOT.jar" restart: unless-stopped depends_on: - executor-pool - - assignment + - roster volumes: - ./:/data/ labels: - "traefik.enable=true" - - "traefik.http.routers.executor1.rule=Host(`executor1.${PUB_IP}.nip.io`)" - - "traefik.http.routers.executor1.service=executor1" - - "traefik.http.services.executor1.loadbalancer.server.port=8084" - - "traefik.http.routers.executor1.tls=true" - - "traefik.http.routers.executor1.entryPoints=web,websecure" - - "traefik.http.routers.executor1.tls.certresolver=le" + - "traefik.http.routers.executor-computation.rule=Host(`executor-computation.${PUB_IP}.nip.io`)" + - "traefik.http.routers.executor-computation.service=executor-computation" + - "traefik.http.services.executor-computation.loadbalancer.server.port=8084" + - "traefik.http.routers.executor-computation.tls=true" + - "traefik.http.routers.executor-computation.entryPoints=web,websecure" + - "traefik.http.routers.executor-computation.tls.certresolver=le" - executor2: + executor-robot: image: openjdk - command: "java -jar /data/executor2-0.0.1-SNAPSHOT.jar" + command: "java -jar /data/executor-robot-0.0.1-SNAPSHOT.jar" restart: unless-stopped depends_on: - executor-pool - - assignment + - roster volumes: - ./:/data/ labels: - "traefik.enable=true" - - "traefik.http.routers.executor2.rule=Host(`executor2.${PUB_IP}.nip.io`)" - - "traefik.http.routers.executor2.service=executor2" - - "traefik.http.services.executor2.loadbalancer.server.port=8085" - - "traefik.http.routers.executor2.tls=true" - - "traefik.http.routers.executor2.entryPoints=web,websecure" - - "traefik.http.routers.executor2.tls.certresolver=le" + - "traefik.http.routers.executor-robot.rule=Host(`executor-robot.${PUB_IP}.nip.io`)" + - "traefik.http.routers.executor-robot.service=executor-robot" + - "traefik.http.services.executor-robot.loadbalancer.server.port=8085" + - "traefik.http.routers.executor-robot.tls=true" + - "traefik.http.routers.executor-robot.entryPoints=web,websecure" + - "traefik.http.routers.executor-robot.tls.certresolver=le" diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index d223887..7290452 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -33,30 +33,33 @@ jobs: key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 - - name: Build with Maven - run: mvn -f assignment/pom.xml --batch-mode --update-snapshots verify - - run: cp ./assignment/target/assignment-0.0.1-SNAPSHOT.jar ./target + - name: Build common library + run: mvn -f common/pom.xml --batch-mode --update-snapshots install - - name: Build with Maven + - name: Build roster service + run: mvn -f roster/pom.xml --batch-mode --update-snapshots verify + - run: cp ./roster/target/roster-0.0.1-SNAPSHOT.jar ./target + + - name: Build executor-pool service run: mvn -f executor-pool/pom.xml --batch-mode --update-snapshots verify - run: cp ./executor-pool/target/executor-pool-0.0.1-SNAPSHOT.jar ./target - - name: Build with Maven + - name: Build executor-base library run: mvn -f executor-base/pom.xml --batch-mode --update-snapshots install - - name: Build with Maven - run: mvn -f executor1/pom.xml --batch-mode --update-snapshots verify - - run: cp ./executor1/target/executor1-0.0.1-SNAPSHOT.jar ./target + - name: Build executor-computation service + run: mvn -f executor-computation/pom.xml --batch-mode --update-snapshots verify + - run: cp ./executor-computation/target/executor-computation-0.0.1-SNAPSHOT.jar ./target - - name: Build with Maven - run: mvn -f executor2/pom.xml --batch-mode --update-snapshots verify - - run: cp ./executor2/target/executor2-0.0.1-SNAPSHOT.jar ./target + - name: Build executor-robot service + run: mvn -f executor-robot/pom.xml --batch-mode --update-snapshots verify + - run: cp ./executor-robot/target/executor-robot-0.0.1-SNAPSHOT.jar ./target - - name: Build with Maven + - name: Build tapas-task service 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 - - name: Build with Maven + - name: Build tapas-auction-house service 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 diff --git a/.github/workflows/ci.assignment.yml b/.github/workflows/ci.assignment.yml deleted file mode 100644 index 394fe54..0000000 --- a/.github/workflows/ci.assignment.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: CI Assignment -on: - push: - branches: [main, dev] - paths: - - "assignment/**" - pull_request: - branches: [main, dev] - paths: - - "assignment/**" - - workflow_dispatch: -jobs: - build: - name: Build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - name: Cache SonarCloud packages - uses: actions/cache@v1 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Cache Maven packages - uses: actions/cache@v1 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -f assignment/pom.xml -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=scs-asse-fs21-group1_tapas-assignment diff --git a/.github/workflows/ci.executor2.yml b/.github/workflows/ci.executor-computation.yml similarity index 75% rename from .github/workflows/ci.executor2.yml rename to .github/workflows/ci.executor-computation.yml index 5ae38f0..5832883 100644 --- a/.github/workflows/ci.executor2.yml +++ b/.github/workflows/ci.executor-computation.yml @@ -1,15 +1,17 @@ -name: CI Executor 2 +name: CI executor-computation on: push: branches: [main, dev] paths: - "executor-base/**" - - "executor2/**" + - "executor-computation/**" + - "common/**" pull_request: branches: [main, dev] paths: - "executor-base/**" - - "executor2/**" + - "executor-computation/**" + - "common/**" workflow_dispatch: jobs: @@ -36,10 +38,12 @@ jobs: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 + - name: Build common + run: mvn -f common/pom.xml -B install - name: Build executorBase run: mvn -f executor-base/pom.xml -B install - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -f executor2/pom.xml -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=scs-asse-fs21-group1_tapas-executor2 + run: mvn -f executor-computation/pom.xml -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=scs-asse-fs21-group1_tapas-executor2 diff --git a/.github/workflows/ci.executor1.yml b/.github/workflows/ci.executor-robot.yml similarity index 77% rename from .github/workflows/ci.executor1.yml rename to .github/workflows/ci.executor-robot.yml index 708d7d4..2ccf607 100644 --- a/.github/workflows/ci.executor1.yml +++ b/.github/workflows/ci.executor-robot.yml @@ -4,12 +4,14 @@ on: branches: [main, dev] paths: - "executor-base/**" - - "executor1/**" + - "executor-robot/**" + - "common/**" pull_request: branches: [main, dev] paths: - "executor-base/**" - - "executor1/**" + - "executor-robot/**" + - "common/**" workflow_dispatch: jobs: @@ -36,10 +38,12 @@ jobs: path: ~/.m2 key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-m2 + - name: Build executorBase + run: mvn -f common/pom.xml -B install - name: Build executorBase run: mvn -f executor-base/pom.xml -B install - name: Build and analyze env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -f executor1/pom.xml -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=scs-asse-fs21-group1_tapas-executor1 + run: mvn -f executor-robot/pom.xml -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=scs-asse-fs21-group1_tapas-executor1 diff --git a/.github/workflows/ci.roster.yml b/.github/workflows/ci.roster.yml new file mode 100644 index 0000000..b38355e --- /dev/null +++ b/.github/workflows/ci.roster.yml @@ -0,0 +1,45 @@ +name: CI Roster +on: + push: + branches: [main, dev] + paths: + - "roster/**" + - "common/**" + pull_request: + branches: [main, dev] + paths: + - "roster/**" + - "common/**" + + workflow_dispatch: +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v1 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build common package + run: mvn -f common/pom.xml -B install + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn -f roster/pom.xml -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=scs-asse-fs21-group1_tapas-assignment diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java index 8fda5ac..66ef496 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java @@ -1,5 +1,7 @@ package ch.unisg.executorbase.executor.adapter.in.web; +import java.util.logging.Logger; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -19,6 +21,8 @@ public class TaskAvailableController { this.taskAvailableUseCase = taskAvailableUseCase; } + Logger logger = Logger.getLogger(TaskAvailableController.class.getName()); + /** * Controller for notification about new events. * @return 200 OK @@ -26,6 +30,8 @@ public class TaskAvailableController { @GetMapping(path = "/newtask/{taskType}", consumes = { "application/json" }) public ResponseEntity retrieveTaskFromTaskList(@PathVariable("taskType") String taskType) { + logger.info("New " + taskType + " available"); + if (ExecutorType.contains(taskType.toUpperCase())) { TaskAvailableCommand command = new TaskAvailableCommand( ExecutorType.valueOf(taskType.toUpperCase())); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java index a5ae910..58f6287 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java @@ -16,8 +16,9 @@ import ch.unisg.executorbase.executor.domain.ExecutionFinishedEvent; public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort { + // TODO url doesn't get mapped bc no autowiring @Value("${roster.url}") - String server; + String server = "http://localhost:8082"; Logger logger = Logger.getLogger(ExecutionFinishedEventAdapter.class.getName()); @@ -28,6 +29,9 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort @Override public void publishExecutionFinishedEvent(ExecutionFinishedEvent event) { + System.out.println("HI"); + System.out.println(server); + String body = new JSONObject() .put("taskID", event.getTaskID()) .put("result", event.getResult()) @@ -41,6 +45,9 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort .POST(HttpRequest.BodyPublishers.ofString(body)) .build(); + + System.out.println(server); + try { client.send(request, HttpResponse.BodyHandlers.ofString()); } catch (InterruptedException e) { @@ -50,7 +57,7 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } - logger.log(Level.INFO, "Finish execution event sent with result: {}", event.getResult()); + logger.log(Level.INFO, "Finish execution event sent with result: {0}", event.getResult()); } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java index 3ed4e37..dd82c81 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java @@ -23,8 +23,9 @@ import org.json.JSONObject; @Primary public class GetAssignmentAdapter implements GetAssignmentPort { + // TODO Not working for now bc it doesn't get autowired @Value("${roster.url}") - String server; + String server = "http://127.0.0.1:8082"; Logger logger = Logger.getLogger(GetAssignmentAdapter.class.getName()); @@ -51,12 +52,15 @@ public class GetAssignmentAdapter implements GetAssignmentPort { try { logger.info("Sending getAssignment Request"); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); - logger.log(Level.INFO, "getAssignment request result:\n {}", response.body()); + logger.log(Level.INFO, "getAssignment request result:\n {0}", response.body()); if (response.body().equals("")) { return null; } JSONObject responseBody = new JSONObject(response.body()); - return new Task(responseBody.getString("taskID"), responseBody.getString("input")); + + String[] input = { "1", "+", "2" }; + // TODO Add input in roster + tasklist + return new Task(responseBody.getString("taskID"), input); } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java index 2dba64f..abc0cf5 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java @@ -22,8 +22,9 @@ import ch.unisg.executorbase.executor.domain.ExecutorType; @Primary public class NotifyExecutorPoolAdapter implements NotifyExecutorPoolPort { - @Value("${executor-pool.url}") - String server; + // TODO Not working for now bc it doesn't get autowired + @Value("${executor.pool.url}") + String server = "http://127.0.0.1:8083"; Logger logger = Logger.getLogger(NotifyExecutorPoolAdapter.class.getName()); @@ -36,7 +37,7 @@ public class NotifyExecutorPoolAdapter implements NotifyExecutorPoolPort { String body = new JSONObject() .put("executorTaskType", executorType) - .put("executorURI", executorURI.getValue()) + .put("executorUri", executorURI.getValue()) .toString(); HttpClient client = HttpClient.newHttpClient(); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java index 122ea4b..b8e8631 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java @@ -23,9 +23,8 @@ public abstract class ExecutorBase { @Getter private ExecutorStatus status; - // TODO Violation of the Dependency Inversion Principle?, but we havn't really got a better solutions to send a http request / access a service from a domain model - // TODO I guess we can implement the execution as a service but there still is the problem with the startup request. - // TODO I guess we can somehow autowire this but I don't know why it's not working :D + // TODO Violation of the Dependency Inversion Principle?, + // TODO do this with only services private final NotifyExecutorPoolPort notifyExecutorPoolPort = new NotifyExecutorPoolAdapter(); private final NotifyExecutorPoolService notifyExecutorPoolService = new NotifyExecutorPoolService(notifyExecutorPoolPort); private final GetAssignmentPort getAssignmentPort = new GetAssignmentAdapter(); @@ -38,8 +37,8 @@ public abstract class ExecutorBase { this.status = ExecutorStatus.STARTING_UP; this.executorType = executorType; // TODO set this automaticly - this.executorURI = new ExecutorURI("localhost:8084"); - + this.executorURI = new ExecutorURI("http://localhost:8084"); + // TODO do this in main // Notify executor-pool about existence. If executor-pools response is successfull start with getting an assignment, else shut down executor. if(!notifyExecutorPoolService.notifyExecutorPool(this.executorURI, this.executorType)) { System.exit(0); @@ -55,6 +54,8 @@ public abstract class ExecutorBase { **/ public void getAssignment() { Task newTask = getAssignmentPort.getAssignment(this.getExecutorType(), this.getExecutorURI()); + System.out.println("New assignment"); + System.out.println(newTask); if (newTask != null) { this.executeTask(newTask); } else { @@ -72,6 +73,8 @@ public abstract class ExecutorBase { task.setResult(execution(task.getInput())); + System.out.println(task.getResult()); + // TODO implement logic if execution was not successful executionFinishedEventPort.publishExecutionFinishedEvent( new ExecutionFinishedEvent(task.getTaskID(), task.getResult(), "SUCCESS")); diff --git a/executor-base/src/main/resources/application.properties b/executor-base/src/main/resources/application.properties index 3eee96a..4316ebf 100644 --- a/executor-base/src/main/resources/application.properties +++ b/executor-base/src/main/resources/application.properties @@ -1,6 +1,6 @@ server.port=8081 roster.url=http://127.0.0.1:8082 -executor-pool.url=http://127.0.0.1:8083 +executor.pool.url=http://127.0.0.1:8083 executor1.url=http://127.0.0.1:8084 executor2.url=http://127.0.0.1:8085 task-list.url=http://127.0.0.1:8081 diff --git a/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java b/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java index eb09699..c3fbb67 100644 --- a/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java +++ b/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java @@ -21,16 +21,18 @@ public class Executor extends ExecutorBase { protected String execution(String... input) { + System.out.println(input); + double result = Double.NaN; int a = Integer.parseInt(input[0]); int b = Integer.parseInt(input[2]); String operation = input[1]; - try { - TimeUnit.SECONDS.sleep(20); - } catch (InterruptedException e) { - e.printStackTrace(); - } + // try { + // TimeUnit.SECONDS.sleep(20); + // } catch (InterruptedException e) { + // e.printStackTrace(); + // } if (operation == "+") { result = a + b; @@ -40,7 +42,9 @@ public class Executor extends ExecutorBase { result = a - b; } + System.out.println("finish"); + return Double.toString(result); } -} \ No newline at end of file +} diff --git a/roster/src/main/java/ch/unisg/roster/RosterApplication.java b/roster/src/main/java/ch/unisg/roster/RosterApplication.java index dd57a5d..bc18c54 100644 --- a/roster/src/main/java/ch/unisg/roster/RosterApplication.java +++ b/roster/src/main/java/ch/unisg/roster/RosterApplication.java @@ -1,13 +1,41 @@ package ch.unisg.roster; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.paho.client.mqttv3.MqttException; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import ch.unisg.roster.roster.adapter.common.clients.TapasMqttClient; +import ch.unisg.roster.roster.adapter.in.messaging.mqtt.ExecutorEventMqttListener; +import ch.unisg.roster.roster.adapter.in.messaging.mqtt.ExecutorEventsMqttDispatcher; + @SpringBootApplication public class RosterApplication { + static Logger logger = Logger.getLogger(RosterApplication.class.getName()); + + public static String MQTT_BROKER = "tcp://localhost:1883"; + public static void main(String[] args) { SpringApplication.run(RosterApplication.class, args); + + bootstrapMarketplaceWithMqtt(); } + /** + * Connects to an MQTT broker, presumably the one used by all TAPAS groups to communicate with + * one another + */ + private static void bootstrapMarketplaceWithMqtt() { + try { + ExecutorEventsMqttDispatcher dispatcher = new ExecutorEventsMqttDispatcher(); + TapasMqttClient client = TapasMqttClient.getInstance(MQTT_BROKER, dispatcher); + client.startReceivingMessages(); + } catch (MqttException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } + } diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java index 1c3cbcd..10e907e 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java @@ -17,6 +17,9 @@ public class ExecutorAddedEventListenerMqttAdapter extends ExecutorEventMqttList @Override public boolean handleEvent(MqttMessage message) { + + System.out.println("New Executor added!"); + String payload = new String(message.getPayload()); try { @@ -25,7 +28,7 @@ public class ExecutorAddedEventListenerMqttAdapter extends ExecutorEventMqttList JsonNode data = new ObjectMapper().readTree(payload); String taskType = data.get("executorTaskType").asText(); - String executorId = data.get("executorURI").asText(); + String executorId = data.get("executorUri").asText(); ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( new ExecutorURI(executorId), diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventsMqttDispatcher.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventsMqttDispatcher.java index caa6202..c1b7649 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventsMqttDispatcher.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorEventsMqttDispatcher.java @@ -16,8 +16,8 @@ public class ExecutorEventsMqttDispatcher { // TODO: Register here your topics and event listener adapters private void initRouter() { - router.put("ch/unisg/tapas-group-tutors/executors/added", new ExecutorAddedEventListenerMqttAdapter()); - router.put("ch/unisg/tapas-group-tutors/executors/removed", new ExecutorRemovedEventListenerMqttAdapter()); + router.put("ch/unisg/tapas/executors/added", new ExecutorAddedEventListenerMqttAdapter()); + router.put("ch/unisg/tapas/executors/removed", new ExecutorRemovedEventListenerMqttAdapter()); } /** diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java index af01346..98b3ac7 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java @@ -1,5 +1,7 @@ package ch.unisg.roster.roster.adapter.in.web; +import java.util.logging.Logger; + import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -18,6 +20,8 @@ public class NewTaskController { this.newTaskUseCase = newTaskUseCase; } + Logger logger = Logger.getLogger(NewTaskController.class.getName()); + /** * Controller which handles the new task event from the tasklist * @return 201 Create or 409 Conflict @@ -25,10 +29,14 @@ public class NewTaskController { @PostMapping(path = "/task", consumes = {"application/task+json"}) public ResponseEntity newTaskController(@RequestBody Task task) { + logger.info("New task with id:" + task.getTaskID()); + NewTaskCommand command = new NewTaskCommand(task.getTaskID(), task.getTaskType()); boolean success = newTaskUseCase.addNewTaskToQueue(command); + logger.info("Could create task: " + success); + if (success) { return new ResponseEntity<>(HttpStatus.CREATED); } diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java index 6a6b7f7..0b16567 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java @@ -34,21 +34,23 @@ public class PublishNewTaskEventAdapter implements NewTaskEventPort { @Override public void publishNewTaskEvent(NewTaskEvent event) { - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(server + "/newtask/" + event.taskType.getValue())) - .GET() - .build(); + System.out.println(server2); + + // HttpClient client = HttpClient.newHttpClient(); + // HttpRequest request = HttpRequest.newBuilder() + // .uri(URI.create(server + "/newtask/" + event.taskType.getValue())) + // .GET() + // .build(); - try { - client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (InterruptedException e) { - logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - } + // try { + // client.send(request, HttpResponse.BodyHandlers.ofString()); + // } catch (InterruptedException e) { + // logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + // Thread.currentThread().interrupt(); + // } catch (IOException e) { + // logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + // } HttpClient client2 = HttpClient.newHttpClient(); HttpRequest request2 = HttpRequest.newBuilder() diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java b/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java index 588ed04..c6e1685 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java @@ -29,6 +29,7 @@ public class NewTaskService implements NewTaskUseCase { public boolean addNewTaskToQueue(NewTaskCommand command) { ExecutorRegistry executorRegistry = ExecutorRegistry.getInstance(); + if (!executorRegistry.containsTaskType(command.getTaskType())) { return false; } From 4c5da8eed6557df6549b2d4334c4357d001c8721 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 15 Nov 2021 12:03:25 +0100 Subject: [PATCH 35/40] fix naming --- .github/workflows/ci.executor-robot.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.executor-robot.yml b/.github/workflows/ci.executor-robot.yml index 2ccf607..7a216aa 100644 --- a/.github/workflows/ci.executor-robot.yml +++ b/.github/workflows/ci.executor-robot.yml @@ -1,4 +1,4 @@ -name: CI Executor 1 +name: CI executor-robot on: push: branches: [main, dev] From bce36196384416cc30a626ba1b595530e224c362 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 16 Nov 2021 15:48:46 +0100 Subject: [PATCH 36/40] renaming --- executor-base/pom.xml | 4 ++-- .../.gitignore | 0 .../.mvn/wrapper/MavenWrapperDownloader.java | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 .../Dockerfile | 0 {executorcomputation => executor-computation}/mvnw | 0 .../mvnw.cmd | 0 .../pom.xml | 6 +++--- .../ExecutorcomputationApplication.java | 0 .../adapter/in/web/TaskAvailableController.java | 0 .../application/service/TaskAvailableService.java | 0 .../executor/domain/Executor.java | 0 .../src/main/resources/application.properties | 0 .../ExecutorcomputationApplicationTests.java | 0 {executorrobot => executor-robot}/.gitignore | 0 .../.mvn/wrapper/MavenWrapperDownloader.java | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 {executorrobot => executor-robot}/Dockerfile | 0 {executorrobot => executor-robot}/mvnw | 0 {executorrobot => executor-robot}/mvnw.cmd | 0 {executorrobot => executor-robot}/pom.xml | 6 +++--- .../executorrobot/ExecutorrobotApplication.java | 0 .../adapter/in/web/TaskAvailableController.java | 0 .../adapter/out/DeleteUserFromRobotAdapter.java | 0 .../adapter/out/InstructionToRobotAdapter.java | 0 .../executor/adapter/out/UserToRobotAdapter.java | 0 .../port/out/DeleteUserFromRobotPort.java | 0 .../port/out/InstructionToRobotPort.java | 0 .../application/port/out/UserToRobotPort.java | 0 .../application/service/TaskAvailableService.java | 0 .../executorrobot/executor/domain/Executor.java | 0 .../src/main/resources/application.properties | 0 .../ExecutorrobotApplicationTests.java | 0 .../ExecutorRemovedEventListenerMqttAdapter.java | 2 +- .../adapter/out/web/PublishNewTaskEventAdapter.java | 2 -- 37 files changed, 9 insertions(+), 11 deletions(-) rename {executorcomputation => executor-computation}/.gitignore (100%) rename {executorcomputation => executor-computation}/.mvn/wrapper/MavenWrapperDownloader.java (100%) rename {executorcomputation => executor-computation}/.mvn/wrapper/maven-wrapper.jar (100%) rename {executorcomputation => executor-computation}/.mvn/wrapper/maven-wrapper.properties (100%) rename {executorcomputation => executor-computation}/Dockerfile (100%) rename {executorcomputation => executor-computation}/mvnw (100%) rename {executorcomputation => executor-computation}/mvnw.cmd (100%) rename {executorcomputation => executor-computation}/pom.xml (94%) rename {executorcomputation => executor-computation}/src/main/java/ch/unisg/executorcomputation/ExecutorcomputationApplication.java (100%) rename {executorcomputation => executor-computation}/src/main/java/ch/unisg/executorcomputation/executor/adapter/in/web/TaskAvailableController.java (100%) rename {executorcomputation => executor-computation}/src/main/java/ch/unisg/executorcomputation/executor/application/service/TaskAvailableService.java (100%) rename {executorcomputation => executor-computation}/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java (100%) rename {executorcomputation => executor-computation}/src/main/resources/application.properties (100%) rename {executorcomputation => executor-computation}/src/test/java/ch/unisg/executorcomputation/ExecutorcomputationApplicationTests.java (100%) rename {executorrobot => executor-robot}/.gitignore (100%) rename {executorrobot => executor-robot}/.mvn/wrapper/MavenWrapperDownloader.java (100%) rename {executorrobot => executor-robot}/.mvn/wrapper/maven-wrapper.jar (100%) rename {executorrobot => executor-robot}/.mvn/wrapper/maven-wrapper.properties (100%) rename {executorrobot => executor-robot}/Dockerfile (100%) rename {executorrobot => executor-robot}/mvnw (100%) rename {executorrobot => executor-robot}/mvnw.cmd (100%) rename {executorrobot => executor-robot}/pom.xml (94%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/ExecutorrobotApplication.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/adapter/in/web/TaskAvailableController.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/adapter/out/DeleteUserFromRobotAdapter.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/adapter/out/InstructionToRobotAdapter.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/adapter/out/UserToRobotAdapter.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/application/port/out/DeleteUserFromRobotPort.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/application/port/out/InstructionToRobotPort.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/application/port/out/UserToRobotPort.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/application/service/TaskAvailableService.java (100%) rename {executorrobot => executor-robot}/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java (100%) rename {executorrobot => executor-robot}/src/main/resources/application.properties (100%) rename {executorrobot => executor-robot}/src/test/java/ch/unisg/executorrobot/ExecutorrobotApplicationTests.java (100%) diff --git a/executor-base/pom.xml b/executor-base/pom.xml index 7893d45..4ea8d2a 100644 --- a/executor-base/pom.xml +++ b/executor-base/pom.xml @@ -9,9 +9,9 @@ ch.unisg - executorbase + executor-base 0.0.1-SNAPSHOT - executorbase + executor-base Demo project for Spring Boot 11 diff --git a/executorcomputation/.gitignore b/executor-computation/.gitignore similarity index 100% rename from executorcomputation/.gitignore rename to executor-computation/.gitignore diff --git a/executorcomputation/.mvn/wrapper/MavenWrapperDownloader.java b/executor-computation/.mvn/wrapper/MavenWrapperDownloader.java similarity index 100% rename from executorcomputation/.mvn/wrapper/MavenWrapperDownloader.java rename to executor-computation/.mvn/wrapper/MavenWrapperDownloader.java diff --git a/executorcomputation/.mvn/wrapper/maven-wrapper.jar b/executor-computation/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from executorcomputation/.mvn/wrapper/maven-wrapper.jar rename to executor-computation/.mvn/wrapper/maven-wrapper.jar diff --git a/executorcomputation/.mvn/wrapper/maven-wrapper.properties b/executor-computation/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from executorcomputation/.mvn/wrapper/maven-wrapper.properties rename to executor-computation/.mvn/wrapper/maven-wrapper.properties diff --git a/executorcomputation/Dockerfile b/executor-computation/Dockerfile similarity index 100% rename from executorcomputation/Dockerfile rename to executor-computation/Dockerfile diff --git a/executorcomputation/mvnw b/executor-computation/mvnw similarity index 100% rename from executorcomputation/mvnw rename to executor-computation/mvnw diff --git a/executorcomputation/mvnw.cmd b/executor-computation/mvnw.cmd similarity index 100% rename from executorcomputation/mvnw.cmd rename to executor-computation/mvnw.cmd diff --git a/executorcomputation/pom.xml b/executor-computation/pom.xml similarity index 94% rename from executorcomputation/pom.xml rename to executor-computation/pom.xml index b9e45ac..c319081 100644 --- a/executorcomputation/pom.xml +++ b/executor-computation/pom.xml @@ -9,9 +9,9 @@ ch.unisg - executorcomputation + executor-computation 0.0.1-SNAPSHOT - executorcomputation + executor-computation Demo project for Spring Boot 11 @@ -42,7 +42,7 @@ ch.unisg - executorbase + executor-base 0.0.1-SNAPSHOT compile diff --git a/executorcomputation/src/main/java/ch/unisg/executorcomputation/ExecutorcomputationApplication.java b/executor-computation/src/main/java/ch/unisg/executorcomputation/ExecutorcomputationApplication.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executorcomputation/ExecutorcomputationApplication.java rename to executor-computation/src/main/java/ch/unisg/executorcomputation/ExecutorcomputationApplication.java diff --git a/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/adapter/in/web/TaskAvailableController.java b/executor-computation/src/main/java/ch/unisg/executorcomputation/executor/adapter/in/web/TaskAvailableController.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/adapter/in/web/TaskAvailableController.java rename to executor-computation/src/main/java/ch/unisg/executorcomputation/executor/adapter/in/web/TaskAvailableController.java diff --git a/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/application/service/TaskAvailableService.java b/executor-computation/src/main/java/ch/unisg/executorcomputation/executor/application/service/TaskAvailableService.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/application/service/TaskAvailableService.java rename to executor-computation/src/main/java/ch/unisg/executorcomputation/executor/application/service/TaskAvailableService.java diff --git a/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java b/executor-computation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java rename to executor-computation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java diff --git a/executorcomputation/src/main/resources/application.properties b/executor-computation/src/main/resources/application.properties similarity index 100% rename from executorcomputation/src/main/resources/application.properties rename to executor-computation/src/main/resources/application.properties diff --git a/executorcomputation/src/test/java/ch/unisg/executorcomputation/ExecutorcomputationApplicationTests.java b/executor-computation/src/test/java/ch/unisg/executorcomputation/ExecutorcomputationApplicationTests.java similarity index 100% rename from executorcomputation/src/test/java/ch/unisg/executorcomputation/ExecutorcomputationApplicationTests.java rename to executor-computation/src/test/java/ch/unisg/executorcomputation/ExecutorcomputationApplicationTests.java diff --git a/executorrobot/.gitignore b/executor-robot/.gitignore similarity index 100% rename from executorrobot/.gitignore rename to executor-robot/.gitignore diff --git a/executorrobot/.mvn/wrapper/MavenWrapperDownloader.java b/executor-robot/.mvn/wrapper/MavenWrapperDownloader.java similarity index 100% rename from executorrobot/.mvn/wrapper/MavenWrapperDownloader.java rename to executor-robot/.mvn/wrapper/MavenWrapperDownloader.java diff --git a/executorrobot/.mvn/wrapper/maven-wrapper.jar b/executor-robot/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from executorrobot/.mvn/wrapper/maven-wrapper.jar rename to executor-robot/.mvn/wrapper/maven-wrapper.jar diff --git a/executorrobot/.mvn/wrapper/maven-wrapper.properties b/executor-robot/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from executorrobot/.mvn/wrapper/maven-wrapper.properties rename to executor-robot/.mvn/wrapper/maven-wrapper.properties diff --git a/executorrobot/Dockerfile b/executor-robot/Dockerfile similarity index 100% rename from executorrobot/Dockerfile rename to executor-robot/Dockerfile diff --git a/executorrobot/mvnw b/executor-robot/mvnw similarity index 100% rename from executorrobot/mvnw rename to executor-robot/mvnw diff --git a/executorrobot/mvnw.cmd b/executor-robot/mvnw.cmd similarity index 100% rename from executorrobot/mvnw.cmd rename to executor-robot/mvnw.cmd diff --git a/executorrobot/pom.xml b/executor-robot/pom.xml similarity index 94% rename from executorrobot/pom.xml rename to executor-robot/pom.xml index 101c268..ca95edf 100644 --- a/executorrobot/pom.xml +++ b/executor-robot/pom.xml @@ -9,9 +9,9 @@ ch.unisg - executorrobot + executor-robot 0.0.1-SNAPSHOT - executorrobot + executor-robot Demo project for Spring Boot 11 @@ -42,7 +42,7 @@ ch.unisg - executorbase + executor-base 0.0.1-SNAPSHOT diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/ExecutorrobotApplication.java b/executor-robot/src/main/java/ch/unisg/executorrobot/ExecutorrobotApplication.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/ExecutorrobotApplication.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/ExecutorrobotApplication.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/in/web/TaskAvailableController.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/in/web/TaskAvailableController.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/in/web/TaskAvailableController.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/in/web/TaskAvailableController.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/DeleteUserFromRobotAdapter.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/DeleteUserFromRobotAdapter.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/DeleteUserFromRobotAdapter.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/DeleteUserFromRobotAdapter.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/InstructionToRobotAdapter.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/InstructionToRobotAdapter.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/InstructionToRobotAdapter.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/InstructionToRobotAdapter.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/UserToRobotAdapter.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/UserToRobotAdapter.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/UserToRobotAdapter.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/UserToRobotAdapter.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/DeleteUserFromRobotPort.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/DeleteUserFromRobotPort.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/DeleteUserFromRobotPort.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/DeleteUserFromRobotPort.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/InstructionToRobotPort.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/InstructionToRobotPort.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/InstructionToRobotPort.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/InstructionToRobotPort.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/UserToRobotPort.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/UserToRobotPort.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/UserToRobotPort.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/UserToRobotPort.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/service/TaskAvailableService.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/service/TaskAvailableService.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/service/TaskAvailableService.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/application/service/TaskAvailableService.java diff --git a/executorrobot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java rename to executor-robot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java diff --git a/executorrobot/src/main/resources/application.properties b/executor-robot/src/main/resources/application.properties similarity index 100% rename from executorrobot/src/main/resources/application.properties rename to executor-robot/src/main/resources/application.properties diff --git a/executorrobot/src/test/java/ch/unisg/executorrobot/ExecutorrobotApplicationTests.java b/executor-robot/src/test/java/ch/unisg/executorrobot/ExecutorrobotApplicationTests.java similarity index 100% rename from executorrobot/src/test/java/ch/unisg/executorrobot/ExecutorrobotApplicationTests.java rename to executor-robot/src/test/java/ch/unisg/executorrobot/ExecutorrobotApplicationTests.java diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java index 71af86d..0d0923a 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java @@ -23,7 +23,7 @@ public class ExecutorRemovedEventListenerMqttAdapter extends ExecutorEventMqttLi // representation that makes sense in the context of your application. JsonNode data = new ObjectMapper().readTree(payload); - String executorId = data.get("executorURI").asText(); + String executorId = data.get("executorUri").asText(); ExecutorRemovedEvent executorRemovedEvent = new ExecutorRemovedEvent( new ExecutorURI(executorId)); diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java index 0b16567..274d639 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java @@ -34,8 +34,6 @@ public class PublishNewTaskEventAdapter implements NewTaskEventPort { @Override public void publishNewTaskEvent(NewTaskEvent event) { - System.out.println(server2); - // HttpClient client = HttpClient.newHttpClient(); // HttpRequest request = HttpRequest.newBuilder() // .uri(URI.create(server + "/newtask/" + event.taskType.getValue())) From 8fba9136b2abfa65aa095852f0abb80860297803 Mon Sep 17 00:00:00 2001 From: reynisson Date: Tue, 16 Nov 2021 17:42:14 +0100 Subject: [PATCH 37/40] Implemented auctioning of tasks workflow in auction house --- .../formats/AuctionJsonRepresentation.java | 5 +- .../common/formats/BidJsonRepresentation.java | 43 ++++++ .../formats/TaskJsonRepresentation.java | 115 +++++++++++++++++ .../BidReceivedEventListenerMqttAdapter.java | 52 ++++++++ .../adapter/in/web/AddBidWebController.java | 38 ++++++ ...PublishAuctionStartedEventMqttAdapter.java | 2 +- .../out/web/AuctionWonEventHttpAdapter.java | 60 +++++++++ .../handler/BidReceivedHandler.java | 17 +++ .../application/port/in/BidReceivedEvent.java | 17 +++ .../port/in/BidReceivedEventHandler.java | 5 + .../unisg/tapas/auctionhouse/domain/Task.java | 122 ++++++++++++++++++ .../src/main/resources/application.properties | 2 +- 12 files changed, 474 insertions(+), 4 deletions(-) create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/BidJsonRepresentation.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/BidReceivedEventListenerMqttAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/AddBidWebController.java rename tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/{websub => mqtt}/PublishAuctionStartedEventMqttAdapter.java (95%) create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/BidReceivedHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEvent.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEventHandler.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Task.java diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java index ea4cf2c..757c8c8 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/AuctionJsonRepresentation.java @@ -8,6 +8,7 @@ import lombok.Getter; import lombok.Setter; import java.sql.Timestamp; +import java.text.SimpleDateFormat; /** * Used to expose a representation of the state of an auction through an interface. This class is @@ -15,7 +16,7 @@ import java.sql.Timestamp; * to modify this class as you see fit! */ public class AuctionJsonRepresentation { - public static final String MEDIA_TYPE = "application/json"; + public static final String MEDIA_TYPE = "application/auction+json"; @Getter @Setter private String auctionId; @@ -56,7 +57,7 @@ public class AuctionJsonRepresentation { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); return mapper.writeValueAsString(representation); } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/BidJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/BidJsonRepresentation.java new file mode 100644 index 0000000..7ae3dda --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/BidJsonRepresentation.java @@ -0,0 +1,43 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.formats; + +import ch.unisg.tapas.auctionhouse.domain.Bid; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; + +import java.text.SimpleDateFormat; + +public class BidJsonRepresentation { + public static final String MEDIA_TYPE = "application/bid+json"; + + @Getter @Setter + private String auctionId; + + @Getter @Setter + private String bidderName; + + @Getter @Setter + private String bidderAuctionHouseUri; + + @Getter @Setter + private String bidderTaskListUri; + + public BidJsonRepresentation() {} + + public BidJsonRepresentation(Bid bid){ + this.auctionId = bid.getAuctionId().getValue(); + this.bidderName = bid.getBidderName().getValue(); + this.bidderAuctionHouseUri = bid.getBidderAuctionHouseUri().getValue().toString(); + this.bidderTaskListUri = bid.getBidderTaskListUri().getValue().toString(); + } + + public static String serialize(Bid bid) throws JsonProcessingException { + BidJsonRepresentation representation = new BidJsonRepresentation(bid); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + return mapper.writeValueAsString(representation); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java new file mode 100644 index 0000000..467c550 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java @@ -0,0 +1,115 @@ +package ch.unisg.tapas.auctionhouse.adapter.common.formats; + +import ch.unisg.tapas.auctionhouse.domain.Task; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.Setter; + +/** + * This class is used to expose and consume representations of tasks through the HTTP interface. The + * representations conform to the custom JSON-based media type "application/task+json". The media type + * is just an identifier and can be registered with + * IANA to promote interoperability. + */ +final public class TaskJsonRepresentation { + // The media type used for this task representation format + public static final String MEDIA_TYPE = "application/task+json"; + + // A task identifier specific to our implementation (e.g., a UUID). This identifier is then used + // to generate the task's URI. URIs are standard uniform identifiers and use a universal syntax + // that can be referenced (and dereferenced) independent of context. In our uniform HTTP API, + // we identify tasks via URIs and not implementation-specific identifiers. + @Getter @Setter + private String taskId; + + // A string that represents the task's name + @Getter + private final String taskName; + + // A string that identifies the task's type. This string could also be a URI (e.g., defined in some + // Web ontology, as we shall see later in the course), but it's not constrained to be a URI. + // The task's type can be used to assign executors to tasks, to decide what tasks to bid for, etc. + @Getter + private final String taskType; + + // The task's status: OPEN, ASSIGNED, RUNNING, or EXECUTED (see Task.Status) + @Getter @Setter + private String taskStatus; + + // If this task is a delegated task (i.e., a shadow of another task), this URI points to the + // original task. Because URIs are standard and uniform, we can just dereference this URI to + // retrieve a representation of the original task. + @Getter @Setter + private String originalTaskUri; + + // The service provider who executes this task. The service provider is a any string that identifies + // a TAPAS group (e.g., tapas-group1). This identifier could also be a URI (if we have a good reason + // for it), but it's not constrained to be a URI. + @Getter @Setter + private String serviceProvider; + + // A string that provides domain-specific input data for this task. In the context of this project, + // we can parse and interpret the input data based on the task's type. + @Getter @Setter + private String inputData; + + // A string that provides domain-specific output data for this task. In the context of this project, + // we can parse and interpret the output data based on the task's type. + @Getter @Setter + private String outputData; + + /** + * Instantiate a task representation with a task name and type. + * + * @param taskName string that represents the task's name + * @param taskType string that represents the task's type + */ + public TaskJsonRepresentation(String taskName, String taskType) { + this.taskName = taskName; + this.taskType = taskType; + + this.taskStatus = null; + this.originalTaskUri = null; + this.serviceProvider = null; + this.inputData = null; + this.outputData = null; + } + + /** + * Instantiate a task representation from a domain concept. + * + * @param task the task + */ + public TaskJsonRepresentation(Task task) { + this(task.getTaskName().getValue(), task.getTaskType().getValue()); + + this.taskId = task.getTaskId().getValue(); + this.taskStatus = task.getTaskStatus().getValue().name(); + + this.originalTaskUri = (task.getOriginalTaskUri() == null) ? + null : task.getOriginalTaskUri().getValue(); + + this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue(); + this.inputData = (task.getInputData() == null) ? null : task.getInputData().getValue(); + this.outputData = (task.getOutputData() == null) ? null : task.getOutputData().getValue(); + } + + /** + * Convenience method used to serialize a task provided as a domain concept in the format exposed + * through the uniform HTTP API. + * + * @param task the task as defined in the domain + * @return a string serialization using the JSON-based representation format defined for tasks + * @throws JsonProcessingException if a runtime exception occurs during object serialization + */ + public static String serialize(Task task) throws JsonProcessingException { + TaskJsonRepresentation representation = new TaskJsonRepresentation(task); + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return mapper.writeValueAsString(representation); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/BidReceivedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/BidReceivedEventListenerMqttAdapter.java new file mode 100644 index 0000000..29f45da --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/BidReceivedEventListenerMqttAdapter.java @@ -0,0 +1,52 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import ch.unisg.tapas.auctionhouse.application.handler.BidReceivedHandler; +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.Bid; +import ch.unisg.tapas.auctionhouse.application.port.in.BidReceivedEvent; +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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import java.net.URI; + +public class BidReceivedEventListenerMqttAdapter extends AuctionEventMqttListener { + private static final Logger LOGGER = LogManager.getLogger(BidReceivedEventListenerMqttAdapter.class); + + @Override + public boolean handleEvent(MqttMessage message){ + String payload = new String(message.getPayload()); + + try { + // Note: this message 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 auctionId = data.get("auctionId").asText(); + String bidderName = data.get("bidderName").asText(); + String bidderAuctionHouseUri = data.get("bidderAuctionHouseUri").asText(); + String bidderTaskListUri = data.get("bidderTaskListUri").asText(); + + BidReceivedEvent bidReceivedEvent = new BidReceivedEvent( new Bid( + new Auction.AuctionId(auctionId), + new Bid.BidderName(bidderName), + new Bid.BidderAuctionHouseUri(URI.create(bidderAuctionHouseUri)), + new Bid.BidderTaskListUri(URI.create(bidderTaskListUri)) + )); + + BidReceivedHandler bidReceivedHandler = new BidReceivedHandler(); + bidReceivedHandler.handleNewBidReceivedEvent(bidReceivedEvent); + } catch (JsonProcessingException | NullPointerException e) { + LOGGER.error(e.getMessage(), e); + return false; + } + + return true; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/AddBidWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/AddBidWebController.java new file mode 100644 index 0000000..3431c8d --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/AddBidWebController.java @@ -0,0 +1,38 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.web; + +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.BidJsonRepresentation; +import ch.unisg.tapas.auctionhouse.application.handler.BidReceivedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.BidReceivedEvent; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Bid; +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 java.net.URI; + +// TODO Fix structure due to MQTT +@RestController +public class AddBidWebController { + @PostMapping(path = "/bid", consumes = BidJsonRepresentation.MEDIA_TYPE) + public ResponseEntity addBid(@RequestBody BidJsonRepresentation payload) { + BidReceivedEvent bidReceivedEvent = new BidReceivedEvent(new Bid( + new Auction.AuctionId(payload.getAuctionId()), + new Bid.BidderName(payload.getBidderName()), + new Bid.BidderAuctionHouseUri(URI.create(payload.getBidderAuctionHouseUri())), + new Bid.BidderTaskListUri(URI.create(payload.getBidderTaskListUri())) + )); + + BidReceivedHandler bidReceivedHandler = new BidReceivedHandler(); + bidReceivedHandler.handleNewBidReceivedEvent(bidReceivedEvent); + + HttpHeaders responseHeaders = new HttpHeaders(); + + return new ResponseEntity<>(responseHeaders, HttpStatus.NO_CONTENT); + } + +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/mqtt/PublishAuctionStartedEventMqttAdapter.java similarity index 95% rename from tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventMqttAdapter.java rename to tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/mqtt/PublishAuctionStartedEventMqttAdapter.java index d5bb0fc..a041b4f 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/websub/PublishAuctionStartedEventMqttAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/messaging/mqtt/PublishAuctionStartedEventMqttAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.tapas.auctionhouse.adapter.out.messaging.websub; +package ch.unisg.tapas.auctionhouse.adapter.out.messaging.mqtt; import ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient; import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java index 26949f2..4583892 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/out/web/AuctionWonEventHttpAdapter.java @@ -1,10 +1,24 @@ package ch.unisg.tapas.auctionhouse.adapter.out.web; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.TaskJsonRepresentation; +import ch.unisg.tapas.auctionhouse.application.handler.AuctionStartedHandler; import ch.unisg.tapas.auctionhouse.application.port.out.AuctionWonEventPort; +import ch.unisg.tapas.auctionhouse.domain.AuctionRegistry; import ch.unisg.tapas.auctionhouse.domain.AuctionWonEvent; +import ch.unisg.tapas.auctionhouse.domain.Task; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + /** * 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, @@ -13,8 +27,54 @@ import org.springframework.stereotype.Component; @Component @Primary public class AuctionWonEventHttpAdapter implements AuctionWonEventPort { + private static final Logger LOGGER = LogManager.getLogger(AuctionWonEventHttpAdapter.class); + + @Value("${tasks.list.uri}") + String server; + @Override public void publishAuctionWonEvent(AuctionWonEvent event) { + try{ + var auction = AuctionRegistry.getInstance().getAuctionById(event.getWinningBid().getAuctionId()); + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(auction.get().getTaskUri().getValue()) + .GET() + .build(); + var response = client.send(request, HttpResponse.BodyHandlers.ofString()); + LOGGER.info(response.body()); + JSONObject responseBody = new JSONObject(response.body()); + + var task = new Task( + new Task.TaskName(responseBody.getString("taskName")), + new Task.TaskType(responseBody.getString("taskType")), + new Task.OriginalTaskUri(auction.get().getTaskUri().getValue().toString()), + new Task.TaskStatus(ch.unisg.tapas.auctionhouse.domain.Task.Status.ASSIGNED), + new Task.TaskId(responseBody.getString("taskId")), + new Task.InputData(responseBody.getString("inputData")), + new Task.ServiceProvider("TODO") + ); + + String body = TaskJsonRepresentation.serialize(task); + LOGGER.info(body); + var postURI = URI.create(auction.get().getAuctionHouseUri().getValue().toString() + "/taskwinner"); + HttpRequest postRequest = HttpRequest.newBuilder() + .uri(postURI) + .header("Content-Type", TaskJsonRepresentation.MEDIA_TYPE) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + var postResponse = client.send(request, HttpResponse.BodyHandlers.ofString()); + + LOGGER.info(postResponse.statusCode()); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (Exception e){ + LOGGER.error(e.getMessage(), e); + } } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/BidReceivedHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/BidReceivedHandler.java new file mode 100644 index 0000000..dc992ac --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/handler/BidReceivedHandler.java @@ -0,0 +1,17 @@ +package ch.unisg.tapas.auctionhouse.application.handler; + +import ch.unisg.tapas.auctionhouse.application.port.in.BidReceivedEvent; +import ch.unisg.tapas.auctionhouse.application.port.in.BidReceivedEventHandler; +import ch.unisg.tapas.auctionhouse.domain.AuctionRegistry; +import org.springframework.stereotype.Component; + +@Component +public class BidReceivedHandler implements BidReceivedEventHandler { + @Override + public boolean handleNewBidReceivedEvent(BidReceivedEvent bidReceivedEvent){ + var auction = AuctionRegistry.getInstance().getAuctionById(bidReceivedEvent.bid.getAuctionId()); + // TODO Handle if auction not there + auction.get().addBid(bidReceivedEvent.bid); + return true; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEvent.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEvent.java new file mode 100644 index 0000000..560f50b --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEvent.java @@ -0,0 +1,17 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +import ch.unisg.tapas.auctionhouse.domain.Bid; +import ch.unisg.tapas.common.SelfValidating; +import lombok.Getter; + +import javax.validation.constraints.NotNull; + +public class BidReceivedEvent extends SelfValidating { + @NotNull + public Bid bid; + + public BidReceivedEvent(Bid bid){ + this.bid = bid; + validateSelf(); + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEventHandler.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEventHandler.java new file mode 100644 index 0000000..b17ac6b --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/BidReceivedEventHandler.java @@ -0,0 +1,5 @@ +package ch.unisg.tapas.auctionhouse.application.port.in; + +public interface BidReceivedEventHandler { + boolean handleNewBidReceivedEvent(BidReceivedEvent bidReceivedEvent); +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Task.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Task.java new file mode 100644 index 0000000..3fd0d89 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Task.java @@ -0,0 +1,122 @@ +package ch.unisg.tapas.auctionhouse.domain; + +import lombok.Getter; +import lombok.Setter; +import lombok.Value; + +import java.util.UUID; + +/**This is a domain entity**/ +public class Task { + public enum Status { + OPEN, ASSIGNED, RUNNING, EXECUTED + } + + @Getter + private final TaskId taskId; + + @Getter + private final TaskName taskName; + + @Getter + private final TaskType taskType; + + @Getter @Setter + public TaskStatus taskStatus; // had to make public for CompleteTaskService + + @Getter + public TaskResult taskResult; // same as above + + @Getter + private final OriginalTaskUri originalTaskUri; + + @Getter @Setter + private ServiceProvider provider; + + @Getter @Setter + private InputData inputData; + + @Getter @Setter + private OutputData outputData; + + public Task(TaskName taskName, TaskType taskType, OriginalTaskUri taskUri) { + this.taskName = taskName; + this.taskType = taskType; + this.taskStatus = new TaskStatus(Status.OPEN); + this.taskId = new TaskId(UUID.randomUUID().toString()); + this.taskResult = new TaskResult(""); + this.originalTaskUri = taskUri; + + this.inputData = null; + this.outputData = null; + } + + public Task(TaskName taskName, TaskType taskType, OriginalTaskUri taskUri, TaskStatus taskStatus, TaskId taskId, InputData inputData, ServiceProvider serviceProvider) { + this.taskName = taskName; + this.taskType = taskType; + this.taskStatus = taskStatus; + this.taskId = taskId; + this.taskResult = new TaskResult(""); + this.originalTaskUri = taskUri; + this.provider = serviceProvider; + + this.inputData = inputData; + this.outputData = new OutputData(""); + } + + 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, null); + } + + protected static Task createTaskWithNameAndTypeAndOriginalTaskUri(TaskName name, TaskType type, + OriginalTaskUri originalTaskUri) { + return new Task(name, type, originalTaskUri); + } + + @Value + public static class TaskId { + String value; + } + + @Value + public static class TaskName { + String value; + } + + @Value + public static class TaskType { + 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; + } + + @Value + public static class TaskResult{ + private String value; + } +} diff --git a/tapas-auction-house/src/main/resources/application.properties b/tapas-auction-house/src/main/resources/application.properties index 706362e..7b94a5a 100644 --- a/tapas-auction-house/src/main/resources/application.properties +++ b/tapas-auction-house/src/main/resources/application.properties @@ -5,7 +5,7 @@ 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/ +tasks.list.uri=http://localhost:8081 application.environment=development auctionhouse.uri=http://localhost:8086 From e869fb96992ad47f9c8192dd5e5622487a571c5b Mon Sep 17 00:00:00 2001 From: reynisson Date: Tue, 16 Nov 2021 18:51:39 +0100 Subject: [PATCH 38/40] Bidding workflow --- .../src/main/resources/application.properties | 2 +- .../tapas/TapasAuctionHouseApplication.java | 2 +- .../formats/TaskJsonRepresentation.java | 9 ++ .../mqtt/AuctionEventsMqttDispatcher.java | 1 + ...uctionStartedEventListenerMqttAdapter.java | 86 +++++++++++++++++++ .../in/web/WinningBidWebController.java | 59 +++++++++++++ 6 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExternalAuctionStartedEventListenerMqttAdapter.java create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/WinningBidWebController.java diff --git a/executor-pool/src/main/resources/application.properties b/executor-pool/src/main/resources/application.properties index 0c9ba7e..c8fd60a 100644 --- a/executor-pool/src/main/resources/application.properties +++ b/executor-pool/src/main/resources/application.properties @@ -1,3 +1,3 @@ server.port=8083 -mqtt.broker.uri=tcp://localhost:1883 +mqtt.broker.uri=tcp://broker.hivemq.com:1883 diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java index 7438032..18c7631 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/TapasAuctionHouseApplication.java @@ -27,7 +27,7 @@ public class TapasAuctionHouseApplication { private ConfigProperties config; public static String RESOURCE_DIRECTORY = "https://api.interactions.ics.unisg.ch/auction-houses/"; - public static String MQTT_BROKER = "tcp://localhost:1883"; + public static String MQTT_BROKER = "tcp://broker.hivemq.com:1883"; public static void main(String[] args) { SpringApplication tapasAuctioneerApp = new SpringApplication(TapasAuctionHouseApplication.class); diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java index 467c550..782978c 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/common/formats/TaskJsonRepresentation.java @@ -112,4 +112,13 @@ final public class TaskJsonRepresentation { return mapper.writeValueAsString(representation); } + + public String serialize() throws JsonProcessingException { + TaskJsonRepresentation representation = this; + + ObjectMapper mapper = new ObjectMapper(); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + + return mapper.writeValueAsString(representation); + } } diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java index 7d30453..91872f2 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java @@ -28,6 +28,7 @@ public class AuctionEventsMqttDispatcher { private void initRouter() { router.put("ch/unisg/tapas/executors/added", new ExecutorAddedEventListenerMqttAdapter()); router.put("ch/unisg/tapas/executors/removed", new ExecutorRemovedEventListenerMqttAdapter()); + router.put("ch/unisg/tapas/auctions", new ExternalAuctionStartedEventListenerMqttAdapter()); } /** diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExternalAuctionStartedEventListenerMqttAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExternalAuctionStartedEventListenerMqttAdapter.java new file mode 100644 index 0000000..5e17d96 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/mqtt/ExternalAuctionStartedEventListenerMqttAdapter.java @@ -0,0 +1,86 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt; + +import ch.unisg.tapas.auctionhouse.adapter.common.formats.BidJsonRepresentation; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.TaskJsonRepresentation; +import ch.unisg.tapas.auctionhouse.application.handler.BidReceivedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.BidReceivedEvent; +import ch.unisg.tapas.auctionhouse.domain.Auction; +import ch.unisg.tapas.auctionhouse.domain.Bid; +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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.springframework.beans.factory.annotation.Value; + +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.sql.Timestamp; + +public class ExternalAuctionStartedEventListenerMqttAdapter extends AuctionEventMqttListener{ + private static final Logger LOGGER = LogManager.getLogger(ExternalAuctionStartedEventListenerMqttAdapter.class); + + @Value("${auction.house.uri}") + String auctionHouseURI; + + @Value("${tasks.list.uri}") + String taskListURI; + + @Override + public boolean handleEvent(MqttMessage message){ + String payload = new String(message.getPayload()); + + try { + // Note: this message 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 auctionId = data.get("auctionId").asText(); + String auctionHouseUri = data.get("auctionHouseUri").asText(); + String taskUri = data.get("taskUri").asText(); + String taskType = data.get("taskType").asText(); + String deadline = data.get("deadline").asText(); + + var capable = ExecutorRegistry.getInstance().containsTaskType(new Auction.AuctionedTaskType(taskType)); + // TODO check deadline + if(capable){ + var bid = new Bid( + new Auction.AuctionId(auctionId), + new Bid.BidderName("Group-1"), + new Bid.BidderAuctionHouseUri(URI.create(auctionHouseUri)), + new Bid.BidderTaskListUri(URI.create(taskListURI)) + ); + + String body = BidJsonRepresentation.serialize(bid); + LOGGER.info(body); + var postURI = URI.create(auctionHouseUri + "/bid"); + HttpRequest postRequest = HttpRequest.newBuilder() + .uri(postURI) + .header("Content-Type", BidJsonRepresentation.MEDIA_TYPE) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + HttpClient client = HttpClient.newHttpClient(); + var postResponse = client.send(postRequest, HttpResponse.BodyHandlers.ofString()); + + LOGGER.info(postResponse.statusCode()); + } + } catch (JsonProcessingException | NullPointerException e) { + LOGGER.error(e.getMessage(), e); + return false; + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (Exception e){ + LOGGER.error(e.getMessage(), e); + } + + return true; + } +} diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/WinningBidWebController.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/WinningBidWebController.java new file mode 100644 index 0000000..9d252b5 --- /dev/null +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/web/WinningBidWebController.java @@ -0,0 +1,59 @@ +package ch.unisg.tapas.auctionhouse.adapter.in.web; + +import ch.unisg.tapas.auctionhouse.adapter.common.formats.AuctionJsonRepresentation; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.BidJsonRepresentation; +import ch.unisg.tapas.auctionhouse.adapter.common.formats.TaskJsonRepresentation; +import ch.unisg.tapas.auctionhouse.adapter.in.messaging.mqtt.ExternalAuctionStartedEventListenerMqttAdapter; +import ch.unisg.tapas.auctionhouse.domain.Task; +import com.fasterxml.jackson.core.JsonProcessingException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.springframework.beans.factory.annotation.Value; +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.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +@RestController +public class WinningBidWebController { + private static final Logger LOGGER = LogManager.getLogger(WinningBidWebController.class); + + @Value("${tasks.list.uri}") + String taskListURI; + + @PostMapping(path = "/taskwinner", consumes = TaskJsonRepresentation.MEDIA_TYPE) + public ResponseEntity winningBid(@RequestBody TaskJsonRepresentation payload){ + try { + var body = payload.serialize(); + LOGGER.info(body); + var postURI = URI.create(taskListURI + "/tasks/"); + HttpRequest postRequest = HttpRequest.newBuilder() + .uri(postURI) + .header("Content-Type", TaskJsonRepresentation.MEDIA_TYPE) + .POST(HttpRequest.BodyPublishers.ofString(body)) + .build(); + + HttpClient client = HttpClient.newHttpClient(); + var postResponse = client.send(postRequest, HttpResponse.BodyHandlers.ofString()); + + LOGGER.info(postResponse.statusCode()); + + + HttpHeaders responseHeaders = new HttpHeaders(); + return new ResponseEntity<>(responseHeaders, HttpStatus.NO_CONTENT); + } + catch ( + IOException | InterruptedException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } + } +} From 5a2cc7a131fb5e07daa0c45155eedd4990fafb88 Mon Sep 17 00:00:00 2001 From: reynisson Date: Tue, 16 Nov 2021 18:53:06 +0100 Subject: [PATCH 39/40] Changed mqtt broker address in roster --- roster/src/main/java/ch/unisg/roster/RosterApplication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roster/src/main/java/ch/unisg/roster/RosterApplication.java b/roster/src/main/java/ch/unisg/roster/RosterApplication.java index bc18c54..bc9ed86 100644 --- a/roster/src/main/java/ch/unisg/roster/RosterApplication.java +++ b/roster/src/main/java/ch/unisg/roster/RosterApplication.java @@ -16,7 +16,7 @@ public class RosterApplication { static Logger logger = Logger.getLogger(RosterApplication.class.getName()); - public static String MQTT_BROKER = "tcp://localhost:1883"; + public static String MQTT_BROKER = "tcp://broker.hivemq.com:1883"; public static void main(String[] args) { SpringApplication.run(RosterApplication.class, args); From 44cc0929bd2d578a6d9b4263253a6258d2a820d1 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 16 Nov 2021 19:09:38 +0100 Subject: [PATCH 40/40] fixes --- .../web/ExecutionFinishedEventAdapter.java | 9 +-- .../adapter/out/web/GetAssignmentAdapter.java | 5 +- .../domain/ExecutionFinishedEvent.java | 6 +- .../executor/domain/ExecutorBase.java | 8 +- .../executorBase/executor/domain/Task.java | 10 ++- .../executor/domain/Executor.java | 55 +++++++++----- .../executor/domain/Executor.java | 2 +- .../adapter/in/web/NewTaskController.java | 3 +- .../in/web/TaskCompletedController.java | 4 +- .../web/PublishTaskAssignedEventAdapter.java | 34 ++++----- .../web/PublishTaskCompletedEventAdapter.java | 13 +++- .../application/port/in/NewTaskCommand.java | 6 +- .../application/service/NewTaskService.java | 2 +- .../ch/unisg/roster/roster/domain/Task.java | 12 ++- .../AddNewTaskToTaskListWebController.java | 11 ++- .../in/web/CompleteTaskWebController.java | 14 +++- .../web/ExternalTaskExecutedWebAdapter.java | 74 +++++++++++++++++++ .../PublishNewTaskAddedEventWebAdapter.java | 1 + .../port/in/AddNewTaskToTaskListCommand.java | 12 ++- .../port/in/CompleteTaskCommand.java | 7 +- .../port/out/ExternalTaskExecutedEvent.java | 28 +++++++ .../out/ExternalTaskExecutedEventHandler.java | 5 ++ .../service/AddNewTaskToTaskListService.java | 35 +++++++-- .../service/CompleteTaskService.java | 16 +++- .../tasks/domain/NewTaskAddedEvent.java | 4 +- .../unisg/tapastasks/tasks/domain/Task.java | 17 +++++ .../tapastasks/tasks/domain/TaskList.java | 9 +++ 27 files changed, 307 insertions(+), 95 deletions(-) create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/ExternalTaskExecutedWebAdapter.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEvent.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEventHandler.java diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java index 58f6287..e618c79 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java @@ -29,12 +29,9 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort @Override public void publishExecutionFinishedEvent(ExecutionFinishedEvent event) { - System.out.println("HI"); - System.out.println(server); - String body = new JSONObject() .put("taskID", event.getTaskID()) - .put("result", event.getResult()) + .put("outputData", event.getOutputData()) .put("status", event.getStatus()) .toString(); @@ -46,8 +43,6 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort .build(); - System.out.println(server); - try { client.send(request, HttpResponse.BodyHandlers.ofString()); } catch (InterruptedException e) { @@ -57,7 +52,7 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } - logger.log(Level.INFO, "Finish execution event sent with result: {0}", event.getResult()); + logger.log(Level.INFO, "Finish execution event sent with result: {0}", event.getOutputData()); } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java index dd82c81..92cea92 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java @@ -58,9 +58,8 @@ public class GetAssignmentAdapter implements GetAssignmentPort { } JSONObject responseBody = new JSONObject(response.body()); - String[] input = { "1", "+", "2" }; - // TODO Add input in roster + tasklist - return new Task(responseBody.getString("taskID"), input); + String inputData = responseBody.getString("inputData"); + return new Task(responseBody.getString("taskID"), inputData); } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java index fea6102..56637c4 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java @@ -8,14 +8,14 @@ public class ExecutionFinishedEvent { private String taskID; @Getter - private String result; + private String outputData; @Getter private String status; - public ExecutionFinishedEvent(String taskID, String result, String status) { + public ExecutionFinishedEvent(String taskID, String outputData, String status) { this.taskID = taskID; - this.result = result; + this.outputData = outputData; this.status = status; } } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java index b8e8631..14582e7 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java @@ -71,13 +71,11 @@ public abstract class ExecutorBase { logger.info("Starting execution"); this.status = ExecutorStatus.EXECUTING; - task.setResult(execution(task.getInput())); - - System.out.println(task.getResult()); + task.setOutputData(execution(task.getInputData())); // TODO implement logic if execution was not successful executionFinishedEventPort.publishExecutionFinishedEvent( - new ExecutionFinishedEvent(task.getTaskID(), task.getResult(), "SUCCESS")); + new ExecutionFinishedEvent(task.getTaskID(), task.getOutputData(), "SUCCESS")); logger.info("Finish execution"); getAssignment(); @@ -87,6 +85,6 @@ public abstract class ExecutorBase { * Implementation of the actual execution method of an executor * @return the execution result **/ - protected abstract String execution(String... input); + protected abstract String execution(String input); } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java index 7dc5783..44595e1 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java @@ -10,14 +10,16 @@ public class Task { @Getter @Setter - private String result; + private String outputData; + // TODO maybe create a value object for inputData so we can make sure it is in the right + // format. @Getter - private String[] input; + private String inputData; - public Task(String taskID, String... input) { + public Task(String taskID, String inputData) { this.taskID = taskID; - this.input = input; + this.inputData= inputData; } } diff --git a/executor-computation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java b/executor-computation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java index c3fbb67..532099b 100644 --- a/executor-computation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java +++ b/executor-computation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java @@ -19,30 +19,45 @@ public class Executor extends ExecutorBase { @Override protected - String execution(String... input) { + String execution(String inputData) { - System.out.println(input); + System.out.println(inputData); - double result = Double.NaN; - int a = Integer.parseInt(input[0]); - int b = Integer.parseInt(input[2]); - String operation = input[1]; - - // try { - // TimeUnit.SECONDS.sleep(20); - // } catch (InterruptedException e) { - // e.printStackTrace(); - // } - - if (operation == "+") { - result = a + b; - } else if (operation == "*") { - result = a * b; - } else if (operation == "-") { - result = a - b; + String operator = ""; + if (inputData.contains("+")) { + operator = "+"; + } else if (inputData.contains("-")) { + operator = "-"; + } else if (inputData.contains("*")) { + operator = "*"; } - System.out.println("finish"); + // System.out.println(operator); + + // double result = Double.NaN; + + // System.out.print(inputData.split("+")); + + // int a = Integer.parseInt(inputData.split(operator)[0]); + // int b = Integer.parseInt(inputData.split(operator)[1]); + + // // try { + // // TimeUnit.SECONDS.sleep(20); + // // } catch (InterruptedException e) { + // // e.printStackTrace(); + // // } + + // if (operator.equalsIgnoreCase("+")) { + // result = a + b; + // } else if (operator.equalsIgnoreCase("*")) { + // result = a * b; + // } else if (operator.equalsIgnoreCase("-")) { + // result = a - b; + // } + + // System.out.println("Result: " + result); + + double result = 0.0; return Double.toString(result); } diff --git a/executor-robot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java index 4124e9e..e83579c 100644 --- a/executor-robot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java +++ b/executor-robot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java @@ -28,7 +28,7 @@ public class Executor extends ExecutorBase { @Override protected - String execution(String... input) { + String execution(String input) { String key = userToRobotPort.userToRobot(); try { diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java index 98b3ac7..7ff5349 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java @@ -31,7 +31,8 @@ public class NewTaskController { logger.info("New task with id:" + task.getTaskID()); - NewTaskCommand command = new NewTaskCommand(task.getTaskID(), task.getTaskType()); + NewTaskCommand command = new NewTaskCommand(task.getTaskID(), task.getTaskType(), + task.getInputData()); boolean success = newTaskUseCase.addNewTaskToQueue(command); diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/TaskCompletedController.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/TaskCompletedController.java index f81db32..5adfd7e 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/TaskCompletedController.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/TaskCompletedController.java @@ -25,9 +25,9 @@ public class TaskCompletedController { **/ @PostMapping(path = "/task/completed", consumes = {"application/json"}) public ResponseEntity addNewTaskTaskToTaskList(@RequestBody Task task) { - + System.out.println("TEST"); TaskCompletedCommand command = new TaskCompletedCommand(task.getTaskID(), - task.getStatus(), task.getResult()); + task.getStatus(), task.getOutputData()); taskCompletedUseCase.taskCompleted(command); diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskAssignedEventAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskAssignedEventAdapter.java index c71e306..2c75a03 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskAssignedEventAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskAssignedEventAdapter.java @@ -32,26 +32,26 @@ public class PublishTaskAssignedEventAdapter implements TaskAssignedEventPort { @Override public void publishTaskAssignedEvent(TaskAssignedEvent event) { - String body = new JSONObject() - .put("taskId", event.taskID) - .toString(); + // String body = new JSONObject() + // .put("taskId", event.taskID) + // .toString(); - HttpClient client = HttpClient.newHttpClient(); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(server + "/tasks/assignTask")) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(body)) - .build(); + // HttpClient client = HttpClient.newHttpClient(); + // HttpRequest request = HttpRequest.newBuilder() + // .uri(URI.create(server + "/tasks/assignTask")) + // .header("Content-Type", "application/task+json") + // .POST(HttpRequest.BodyPublishers.ofString(body)) + // .build(); - try { - client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (InterruptedException e) { - logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - Thread.currentThread().interrupt(); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - } + // try { + // client.send(request, HttpResponse.BodyHandlers.ofString()); + // } catch (InterruptedException e) { + // logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + // Thread.currentThread().interrupt(); + // } catch (IOException e) { + // logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + // } } } diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskCompletedEventAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskCompletedEventAdapter.java index 7038291..3773621 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskCompletedEventAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskCompletedEventAdapter.java @@ -32,17 +32,22 @@ public class PublishTaskCompletedEventAdapter implements TaskCompletedEventPort @Override public void publishTaskCompleted(TaskCompletedEvent event) { + System.out.println("PublishTaskCompletedEventAdapter.publishTaskCompleted()"); + System.out.print(server); + String body = new JSONObject() .put("taskId", event.taskID) .put("status", event.status) - .put("taskResult", event.result) + .put("outputData", event.result) .toString(); + System.out.println(event.taskID); + HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(server + "/tasks/completeTask")) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(body)) + .uri(URI.create(server + "/tasks/completeTask/" + event.taskID)) + .header("Content-Type", "application/task+json") + .GET() .build(); diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskCommand.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskCommand.java index 92a7403..5db2b9f 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskCommand.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskCommand.java @@ -17,9 +17,13 @@ public class NewTaskCommand extends SelfValidating { @NotNull private final ExecutorType taskType; - public NewTaskCommand(String taskID, ExecutorType taskType) { + @NotNull + private final String inputData; + + public NewTaskCommand(String taskID, ExecutorType taskType, String inputData) { this.taskID = taskID; this.taskType = taskType; + this.inputData = inputData; this.validateSelf(); } } diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java b/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java index c6e1685..c1aab5c 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java @@ -34,7 +34,7 @@ public class NewTaskService implements NewTaskUseCase { return false; } - Task task = new Task(command.getTaskID(), command.getTaskType()); + Task task = new Task(command.getTaskID(), command.getTaskType(), command.getInputData()); Roster.getInstance().addTaskToQueue(task); diff --git a/roster/src/main/java/ch/unisg/roster/roster/domain/Task.java b/roster/src/main/java/ch/unisg/roster/roster/domain/Task.java index 40ef9fa..ee30763 100644 --- a/roster/src/main/java/ch/unisg/roster/roster/domain/Task.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/Task.java @@ -14,7 +14,11 @@ public class Task { @Getter @Setter - private String result; + private String inputData; + + @Getter + @Setter + private String outputData; @Getter @Setter @@ -30,6 +34,12 @@ public class Task { this.taskType = taskType; } + public Task(String taskID, ExecutorType taskType, String inputData) { + this.taskID = taskID; + this.taskType = taskType; + this.inputData = inputData; + } + public Task() {} } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java index 234dcde..15c3ebb 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java @@ -55,16 +55,15 @@ public class AddNewTaskToTaskListWebController { (payload.getOriginalTaskUri() == null) ? Optional.empty() : Optional.of(new Task.OriginalTaskUri(payload.getOriginalTaskUri())); + Optional inputData = + (payload.getInputData() == null) ? Optional.empty() + : Optional.of(new Task.InputData(payload.getInputData())); + AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(taskName, taskType, - originalTaskUriOptional); + originalTaskUriOptional, inputData); 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, TaskJsonRepresentation.MEDIA_TYPE); diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java index ec2b7b0..fa5578b 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java @@ -8,8 +8,11 @@ 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.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; @@ -23,12 +26,17 @@ public class CompleteTaskWebController { this.completeTaskUseCase = completeTaskUseCase; } - @PostMapping(path = "/tasks/completeTask", consumes = {TaskJsonRepresentation.MEDIA_TYPE}) - public ResponseEntity completeTask (@RequestBody Task task){ + @GetMapping(path = "/tasks/completeTask/{taskId}") + public ResponseEntity completeTask (@PathVariable("taskId") String taskId){ + + System.out.println("completeTask"); + System.out.println(taskId); + + String taskResult = "0"; try { CompleteTaskCommand command = new CompleteTaskCommand( - task.getTaskId(), task.getTaskResult() + new Task.TaskId(taskId), new Task.OutputData(taskResult) ); Task updateATask = completeTaskUseCase.completeTask(command); diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/ExternalTaskExecutedWebAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/ExternalTaskExecutedWebAdapter.java new file mode 100644 index 0000000..8e6f106 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/ExternalTaskExecutedWebAdapter.java @@ -0,0 +1,74 @@ +package ch.unisg.tapastasks.tasks.adapter.out.web; + +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.logging.Level; +import java.util.logging.Logger; + +import com.github.fge.jsonpatch.JsonPatch; +import com.github.fge.jsonpatch.JsonPatchOperation; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation; +import ch.unisg.tapastasks.tasks.application.port.out.ExternalTaskExecutedEvent; +import ch.unisg.tapastasks.tasks.application.port.out.ExternalTaskExecutedEventHandler; + +@Component +@Primary +public class ExternalTaskExecutedWebAdapter implements ExternalTaskExecutedEventHandler { + + Logger logger = Logger.getLogger(ExternalTaskExecutedWebAdapter.class.getName()); + + /** + * Updates an external task which got executed in our system. + **/ + @Override + public void handleEvent(ExternalTaskExecutedEvent externalTaskExecutedEvent) { + + JSONObject op1; + JSONObject op2; + try { + op1 = new JSONObject() + .put("op", "replace") + .put("path", "/taskStatus") + .put("value", "EXECUTED"); + + op2 = new JSONObject() + .put("op", "add") + .put("path", "/outputData") + .put("value", "0"); + } catch (JSONException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + return; + } + + String body = new JSONArray().put(op1).put(op2).toString(); + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(externalTaskExecutedEvent.getOriginalTaskUri().getValue())) + .header("Content-Type", "application/json") + .method("PATCH", HttpRequest.BodyPublishers.ofString(body)) + .build(); + + + try { + client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); + } + + } + +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java index 569b1e9..80b3d09 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java @@ -29,6 +29,7 @@ public class PublishNewTaskAddedEventWebAdapter implements NewTaskAddedEventPort var values = new HashMap() {{ put("taskID", event.taskId); put("taskType", event.taskType); + put("inputData", event.inputData); }}; var objectMapper = new ObjectMapper(); diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java index fbb66ed..d307a1f 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java @@ -19,11 +19,19 @@ public class AddNewTaskToTaskListCommand extends SelfValidating originalTaskUri; - public AddNewTaskToTaskListCommand(Task.TaskName taskName, Task.TaskType taskType, - Optional originalTaskUri) { + @Getter + private final Optional inputData; + + public AddNewTaskToTaskListCommand( + Task.TaskName taskName, + Task.TaskType taskType, + Optional originalTaskUri, + Optional inputData + ) { this.taskName = taskName; this.taskType = taskType; this.originalTaskUri = originalTaskUri; + this.inputData = inputData; this.validateSelf(); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/CompleteTaskCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/CompleteTaskCommand.java index 0634165..238abd2 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/CompleteTaskCommand.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/CompleteTaskCommand.java @@ -1,6 +1,7 @@ package ch.unisg.tapastasks.tasks.application.port.in; import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task.OutputData; import ch.unisg.tapastasks.tasks.domain.Task.TaskId; import ch.unisg.tapastasks.tasks.domain.Task.TaskResult; import lombok.Value; @@ -13,11 +14,11 @@ public class CompleteTaskCommand extends SelfValidating { private final TaskId taskId; @NotNull - private final TaskResult taskResult; + private final OutputData outputData; - public CompleteTaskCommand(TaskId taskId, TaskResult taskResult){ + public CompleteTaskCommand(TaskId taskId, OutputData outputData){ this.taskId = taskId; - this.taskResult = taskResult; + this.outputData = outputData; this.validateSelf(); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEvent.java new file mode 100644 index 0000000..43bad47 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEvent.java @@ -0,0 +1,28 @@ +package ch.unisg.tapastasks.tasks.application.port.out; + +import javax.validation.constraints.NotNull; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task; +import lombok.Getter; +import lombok.Value; + +@Value +public class ExternalTaskExecutedEvent extends SelfValidating { + @NotNull + private final Task.TaskId taskId; + + @Getter + private final Task.OriginalTaskUri originalTaskUri; + + @Getter + private final Task.OutputData outputData; + + public ExternalTaskExecutedEvent(Task.TaskId taskId, Task.OriginalTaskUri originalTaskUri, Task.OutputData outputData) { + this.taskId = taskId; + this.originalTaskUri = originalTaskUri; + this.outputData = outputData; + + this.validateSelf(); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEventHandler.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEventHandler.java new file mode 100644 index 0000000..90bff49 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/ExternalTaskExecutedEventHandler.java @@ -0,0 +1,5 @@ +package ch.unisg.tapastasks.tasks.application.port.out; + +public interface ExternalTaskExecutedEventHandler { + void handleEvent(ExternalTaskExecutedEvent externalTaskExecutedEvent); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java index 70818b1..26234ce 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java @@ -22,12 +22,26 @@ public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) { TaskList taskList = TaskList.getTapasTaskList(); - 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()); + Task newTask; + + System.out.println("TEST:"); + System.out.println(command.getInputData().get()); + + if (command.getOriginalTaskUri().isPresent() && command.getInputData().isPresent()) { + System.out.println("TEST2:"); + newTask = taskList.addNewTaskWithNameAndTypeAndOriginalTaskUriAndInputData(command.getTaskName(), + command.getTaskType(), command.getOriginalTaskUri().get(), command.getInputData().get()); + } else if (command.getOriginalTaskUri().isPresent()) { + newTask = taskList.addNewTaskWithNameAndTypeAndOriginalTaskUri(command.getTaskName(), + command.getTaskType(), command.getOriginalTaskUri().get()); + } else if (command.getOriginalTaskUri().isPresent()) { + newTask = null; + } else { + newTask = taskList.addNewTaskWithNameAndType(command.getTaskName(), command.getTaskType()); + } + + System.out.println("TEST"); + System.out.println(newTask.getInputData()); //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. @@ -35,8 +49,13 @@ public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase //not recommended to emit a domain event via an application service! You should first emit the domain event in //the core and then the integration event in the application layer. if (newTask != null) { - NewTaskAddedEvent newTaskAdded = new NewTaskAddedEvent(newTask.getTaskName().getValue(), - taskList.getTaskListName().getValue(), newTask.getTaskId().getValue(), newTask.getTaskType().getValue()); + NewTaskAddedEvent newTaskAdded = new NewTaskAddedEvent( + newTask.getTaskName().getValue(), + taskList.getTaskListName().getValue(), + newTask.getTaskId().getValue(), + newTask.getTaskType().getValue(), + newTask.getInputData().getValue() + ); newTaskAddedEventPort.publishNewTaskAddedEvent(newTaskAdded); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java index 0e7f817..df22421 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java @@ -2,10 +2,11 @@ package ch.unisg.tapastasks.tasks.application.service; import ch.unisg.tapastasks.tasks.application.port.in.CompleteTaskCommand; import ch.unisg.tapastasks.tasks.application.port.in.CompleteTaskUseCase; +import ch.unisg.tapastasks.tasks.application.port.out.ExternalTaskExecutedEvent; +import ch.unisg.tapastasks.tasks.application.port.out.ExternalTaskExecutedEventHandler; import ch.unisg.tapastasks.tasks.domain.Task; import ch.unisg.tapastasks.tasks.domain.Task.*; import ch.unisg.tapastasks.tasks.domain.TaskList; -import lombok.Getter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -17,15 +18,26 @@ import java.util.Optional; @Transactional public class CompleteTaskService implements CompleteTaskUseCase { + private final ExternalTaskExecutedEventHandler externalTaskExecutedEventHandler; + @Override public Task completeTask(CompleteTaskCommand command){ TaskList taskList = TaskList.getTapasTaskList(); Optional updatedTask = taskList.retrieveTaskById(command.getTaskId()); Task newTask = updatedTask.get(); - newTask.taskResult = new TaskResult(command.getTaskResult().getValue()); + newTask.taskResult = new TaskResult(command.getOutputData().getValue()); newTask.taskStatus = new TaskStatus(Task.Status.EXECUTED); + if (!newTask.getOriginalTaskUri().getValue().equalsIgnoreCase("")) { + ExternalTaskExecutedEvent event = new ExternalTaskExecutedEvent( + newTask.getTaskId(), + newTask.getOriginalTaskUri(), + newTask.getOutputData() + ); + externalTaskExecutedEventHandler.handleEvent(event); + } + return newTask; } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/NewTaskAddedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/NewTaskAddedEvent.java index a4703f2..049c2fb 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/NewTaskAddedEvent.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/NewTaskAddedEvent.java @@ -6,12 +6,14 @@ public class NewTaskAddedEvent { public String taskListName; public String taskId; public String taskType; + public String inputData; - public NewTaskAddedEvent(String taskName, String taskListName, String taskId, String taskType) { + public NewTaskAddedEvent(String taskName, String taskListName, String taskId, String taskType, String inputData) { this.taskName = taskName; this.taskListName = taskListName; this.taskId = taskId; this.taskType = taskType; + this.inputData = inputData; } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java index 4893045..d07f0f1 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java @@ -51,6 +51,18 @@ public class Task { this.outputData = null; } + public Task(TaskName taskName, TaskType taskType, OriginalTaskUri taskUri, InputData inputData) { + this.taskName = taskName; + this.taskType = taskType; + this.taskStatus = new TaskStatus(Status.OPEN); + this.taskId = new TaskId(UUID.randomUUID().toString()); + this.taskResult = new TaskResult(""); + this.originalTaskUri = taskUri; + + this.inputData = inputData; + 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()); @@ -62,6 +74,11 @@ public class Task { return new Task(name, type, originalTaskUri); } + protected static Task createTaskWithNameAndTypeAndOriginalTaskUriAndInputData(TaskName name, TaskType type, + OriginalTaskUri originalTaskUri, InputData inputData) { + return new Task(name, type, originalTaskUri, inputData); + } + @Value public static class TaskId { String value; diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java index d7bb877..72160e8 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java @@ -48,6 +48,15 @@ public class TaskList { return newTask; } + public Task addNewTaskWithNameAndTypeAndOriginalTaskUriAndInputData(Task.TaskName name, Task.TaskType type, + Task.OriginalTaskUri originalTaskUri, Task.InputData inputData) { + Task newTask = Task.createTaskWithNameAndTypeAndOriginalTaskUriAndInputData(name, type, originalTaskUri, inputData); + 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

bCQZLZ=*V@|Ixd#LUbR@*~Do7Z_%{wu1!PRSf$F5|2pu#yN-BwGQ zAI?$^;$ik{+^=%}(`q-`y|l-b>$ZLt%{X6^L;qh|b4R?efGI9t}G@!Z0ip>Ew_ntSfE-ZeDAu$C)H zNlA)dG3_P;#>PM>!;SH)gFaA-dgGEPA9;jN#kkq@_=SteP?+=iqm4InF#gNFZ`4+! zC7L-s{97M%2G^!L+ty5!GSi$0M(3G^aYJ_!K`{1lVwSq6*b^)=natwXSh0y$j!QWE zBE;d|Ul4}@5qK1QZSKu6^gY4U8s?=|+w{2m{=^`9LHmX0fzy~;VCdxZYE%JxvAJ9B^S`_pfPU04N(x9Roo*SaZRffA`C zvWWbSN|4K%&ug7otfhXKOZb!~&E-62LH4iQEI4cGeH9J$c_XMfW6YN7-#g>ezhzvV z*0|Ds=IOpa{YGL|u(mm=GY1%8!I(D9MQ3)r$*^M@xPqW-l+1R6XS*aR%-n2mqPRZ_ z_ze82{{UvMP<9Pbxx_L}Q;gj5=Um2HrX9BXe{z6KSo>|tcSQLnv)}PY7dHksI%Iqj z*;M)TR~BJSc_xnh|0jCE+_XyU2&ThcWsYf+Je^=s~tk =#z6G7eW-KM>%~;6jC2g|I9u4& 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 zWId=eE{)json 20210307 + + + ch.unisg + common + 0.0.1-SNAPSHOT + diff --git a/executor-base/src/main/java/ch/unisg/executorBase/Executor1Application.java b/executor-base/src/main/java/ch/unisg/executorBase/Executor1Application.java deleted file mode 100644 index 9bd3fa5..0000000 --- a/executor-base/src/main/java/ch/unisg/executorBase/Executor1Application.java +++ /dev/null @@ -1,13 +0,0 @@ -package ch.unisg.executorBase; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class Executor1Application { - - public static void main(String[] args) { - SpringApplication.run(Executor1Application.class, args); - } - -} diff --git a/executor-base/src/main/java/ch/unisg/executorBase/common/SelfValidating.java b/executor-base/src/main/java/ch/unisg/executorBase/common/SelfValidating.java deleted file mode 100644 index 5119ac5..0000000 --- a/executor-base/src/main/java/ch/unisg/executorBase/common/SelfValidating.java +++ /dev/null @@ -1,30 +0,0 @@ -package ch.unisg.executorBase.common; - -import javax.validation.ConstraintViolation; -import javax.validation.ConstraintViolationException; -import javax.validation.Validation; -import javax.validation.Validator; -import javax.validation.ValidatorFactory; -import java.util.Set; - -public class SelfValidating { - - private Validator validator; - - public SelfValidating() { - ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); - validator = factory.getValidator(); - } - - /** - * Evaluates all Bean Validations on the attributes of this - * instance. - */ - protected void validateSelf() { - @SuppressWarnings("unchecked") - Set> violations = validator.validate((T) this); - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); - } - } -} diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java index 182f2ba..6c1c659 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.adapter.in.web; +package ch.unisg.executorbase.executor.adapter.in.web; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -7,9 +7,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableCommand; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableUseCase; +import ch.unisg.executorbase.executor.domain.ExecutorType; @RestController public class TaskAvailableController { diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java index 971e583..0947e4f 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.adapter.out.web; +package ch.unisg.executorbase.executor.adapter.out.web; import java.io.IOException; import java.net.URI; @@ -9,13 +9,15 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; -import ch.unisg.executorBase.executor.application.port.out.ExecutionFinishedEventPort; -import ch.unisg.executorBase.executor.domain.ExecutionFinishedEvent; +import ch.unisg.executorbase.executor.application.port.out.ExecutionFinishedEventPort; +import ch.unisg.executorbase.executor.domain.ExecutionFinishedEvent; public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort { - String server = "http://127.0.0.1:8082"; + @Value("${roster.url}") + String server; Logger logger = Logger.getLogger(ExecutionFinishedEventAdapter.class.getName()); @@ -37,10 +39,11 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort try { client.send(request, HttpResponse.BodyHandlers.ofString()); - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } System.out.println("Finish execution event sent with result:" + event.getResult()); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java index 05852fa..14976f2 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.adapter.out.web; +package ch.unisg.executorbase.executor.adapter.out.web; import java.io.IOException; import java.net.URI; @@ -8,12 +8,14 @@ import java.net.http.HttpResponse; import java.util.logging.Level; import java.util.logging.Logger; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; -import ch.unisg.executorBase.executor.application.port.out.GetAssignmentPort; -import ch.unisg.executorBase.executor.domain.ExecutorType; -import ch.unisg.executorBase.executor.domain.Task; +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.executorbase.executor.application.port.out.GetAssignmentPort; +import ch.unisg.executorbase.executor.domain.ExecutorType; +import ch.unisg.executorbase.executor.domain.Task; import org.json.JSONObject; @@ -21,17 +23,17 @@ import org.json.JSONObject; @Primary public class GetAssignmentAdapter implements GetAssignmentPort { - String server = "http://127.0.0.1:8082"; + @Value("${roster.url}") + String server; Logger logger = Logger.getLogger(GetAssignmentAdapter.class.getName()); @Override - public Task getAssignment(ExecutorType executorType, String ip, int port) { + public Task getAssignment(ExecutorType executorType, ExecutorURI executorURI) { String body = new JSONObject() .put("executorType", executorType) - .put("ip", ip) - .put("port", port) + .put("executorURI", executorURI.getValue()) .toString(); HttpClient client = HttpClient.newHttpClient(); @@ -49,10 +51,11 @@ public class GetAssignmentAdapter implements GetAssignmentPort { return new Task(new JSONObject(response.body()).getString("taskID")); - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } return null; diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java index 720b015..cad09a9 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.adapter.out.web; +package ch.unisg.executorbase.executor.adapter.out.web; import java.io.IOException; import java.net.URI; @@ -9,28 +9,30 @@ import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import ch.unisg.executorBase.executor.application.port.out.NotifyExecutorPoolPort; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.executorbase.executor.application.port.out.NotifyExecutorPoolPort; +import ch.unisg.executorbase.executor.domain.ExecutorType; @Component @Primary public class NotifyExecutorPoolAdapter implements NotifyExecutorPoolPort { - String server = "http://127.0.0.1:8083"; + @Value("${executor-pool.url}") + String server; Logger logger = Logger.getLogger(NotifyExecutorPoolAdapter.class.getName()); @Override - public boolean notifyExecutorPool(String ip, int port, ExecutorType executorType) { + public boolean notifyExecutorPool(ExecutorURI executorURI, ExecutorType executorType) { String body = new JSONObject() .put("executorTaskType", executorType) - .put("executorIp", ip) - .put("executorPort", Integer.toString(port)) + .put("executorURI", executorURI.getValue()) .toString(); HttpClient client = HttpClient.newHttpClient(); @@ -45,10 +47,11 @@ public class NotifyExecutorPoolAdapter implements NotifyExecutorPoolPort { if (response.statusCode() == HttpStatus.CREATED.value()) { return true; } - } catch (IOException | InterruptedException e) { + } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); - // Restore interrupted state... Thread.currentThread().interrupt(); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } return false; diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableCommand.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableCommand.java index cfa32bb..57bee60 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableCommand.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableCommand.java @@ -1,10 +1,9 @@ -package ch.unisg.executorBase.executor.application.port.in; - -import ch.unisg.executorBase.common.SelfValidating; -import ch.unisg.executorBase.executor.domain.ExecutorType; +package ch.unisg.executorbase.executor.application.port.in; import javax.validation.constraints.NotNull; +import ch.unisg.common.validation.SelfValidating; +import ch.unisg.executorbase.executor.domain.ExecutorType; import lombok.Value; @Value diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableUseCase.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableUseCase.java index cc5215f..5e000da 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableUseCase.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/in/TaskAvailableUseCase.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.application.port.in; +package ch.unisg.executorbase.executor.application.port.in; public interface TaskAvailableUseCase { void newTaskAvailable(TaskAvailableCommand command); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/ExecutionFinishedEventPort.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/ExecutionFinishedEventPort.java index 1bf668e..ef65922 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/ExecutionFinishedEventPort.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/ExecutionFinishedEventPort.java @@ -1,6 +1,6 @@ -package ch.unisg.executorBase.executor.application.port.out; +package ch.unisg.executorbase.executor.application.port.out; -import ch.unisg.executorBase.executor.domain.ExecutionFinishedEvent; +import ch.unisg.executorbase.executor.domain.ExecutionFinishedEvent; public interface ExecutionFinishedEventPort { void publishExecutionFinishedEvent(ExecutionFinishedEvent event); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/GetAssignmentPort.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/GetAssignmentPort.java index 79d3a0a..95dc15d 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/GetAssignmentPort.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/GetAssignmentPort.java @@ -1,8 +1,9 @@ -package ch.unisg.executorBase.executor.application.port.out; +package ch.unisg.executorbase.executor.application.port.out; -import ch.unisg.executorBase.executor.domain.ExecutorType; -import ch.unisg.executorBase.executor.domain.Task; +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.executorbase.executor.domain.ExecutorType; +import ch.unisg.executorbase.executor.domain.Task; public interface GetAssignmentPort { - Task getAssignment(ExecutorType executorType, String ip, int port); + Task getAssignment(ExecutorType executorType, ExecutorURI executorURI); } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/NotifyExecutorPoolPort.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/NotifyExecutorPoolPort.java index 6d41ab4..1d4d3d3 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/NotifyExecutorPoolPort.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/port/out/NotifyExecutorPoolPort.java @@ -1,7 +1,8 @@ -package ch.unisg.executorBase.executor.application.port.out; +package ch.unisg.executorbase.executor.application.port.out; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.executorbase.executor.domain.ExecutorType; public interface NotifyExecutorPoolPort { - boolean notifyExecutorPool(String ip, int port, ExecutorType executorType); + boolean notifyExecutorPool(ExecutorURI executorURI, ExecutorType executorType); } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/NotifyExecutorPoolService.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/NotifyExecutorPoolService.java index a5ccb64..aee3142 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/NotifyExecutorPoolService.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/NotifyExecutorPoolService.java @@ -1,7 +1,8 @@ -package ch.unisg.executorBase.executor.application.service; +package ch.unisg.executorbase.executor.application.service; -import ch.unisg.executorBase.executor.application.port.out.NotifyExecutorPoolPort; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.executorbase.executor.application.port.out.NotifyExecutorPoolPort; +import ch.unisg.executorbase.executor.domain.ExecutorType; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -9,7 +10,7 @@ public class NotifyExecutorPoolService { private final NotifyExecutorPoolPort notifyExecutorPoolPort; - public boolean notifyExecutorPool(String ip, int port, ExecutorType executorType) { - return notifyExecutorPoolPort.notifyExecutorPool(ip, port, executorType); + public boolean notifyExecutorPool(ExecutorURI executorURI, ExecutorType executorType) { + return notifyExecutorPoolPort.notifyExecutorPool(executorURI, executorType); } } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java index a4f5e6e..c770985 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java @@ -1,9 +1,9 @@ -package ch.unisg.executorBase.executor.application.service; +package ch.unisg.executorbase.executor.application.service; import org.springframework.stereotype.Component; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableCommand; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableUseCase; import lombok.RequiredArgsConstructor; import javax.transaction.Transactional; diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java index 31fd0e6..fea6102 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutionFinishedEvent.java @@ -1,12 +1,12 @@ -package ch.unisg.executorBase.executor.domain; +package ch.unisg.executorbase.executor.domain; import lombok.Getter; public class ExecutionFinishedEvent { - + @Getter private String taskID; - + @Getter private String result; diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java index c9df1a8..7644887 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java @@ -1,26 +1,24 @@ -package ch.unisg.executorBase.executor.domain; +package ch.unisg.executorbase.executor.domain; -import ch.unisg.executorBase.executor.application.port.out.ExecutionFinishedEventPort; -import ch.unisg.executorBase.executor.application.port.out.GetAssignmentPort; -import ch.unisg.executorBase.executor.application.port.out.NotifyExecutorPoolPort; - -import ch.unisg.executorBase.executor.adapter.out.web.ExecutionFinishedEventAdapter; -import ch.unisg.executorBase.executor.adapter.out.web.GetAssignmentAdapter; -import ch.unisg.executorBase.executor.adapter.out.web.NotifyExecutorPoolAdapter; -import ch.unisg.executorBase.executor.application.service.NotifyExecutorPoolService; +import ch.unisg.common.exception.InvalidExecutorURIException; +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.executorbase.executor.adapter.out.web.ExecutionFinishedEventAdapter; +import ch.unisg.executorbase.executor.adapter.out.web.GetAssignmentAdapter; +import ch.unisg.executorbase.executor.adapter.out.web.NotifyExecutorPoolAdapter; +import ch.unisg.executorbase.executor.application.port.out.ExecutionFinishedEventPort; +import ch.unisg.executorbase.executor.application.port.out.GetAssignmentPort; +import ch.unisg.executorbase.executor.application.port.out.NotifyExecutorPoolPort; +import ch.unisg.executorbase.executor.application.service.NotifyExecutorPoolService; import lombok.Getter; public abstract class ExecutorBase { @Getter - private String ip; + private ExecutorURI executorURI; @Getter private ExecutorType executorType; - @Getter - private int port; - @Getter private ExecutorStatus status; @@ -34,12 +32,17 @@ public abstract class ExecutorBase { public ExecutorBase(ExecutorType executorType) { System.out.println("Starting Executor"); // TODO set this automaticly - this.ip = "localhost"; - this.port = 8084; + try { + this.executorURI = new ExecutorURI("localhost:8084"); + } catch (InvalidExecutorURIException e) { + // Shutdown system if ip or port is not valid + System.exit(1); + } + this.executorType = executorType; this.status = ExecutorStatus.STARTING_UP; - if(!notifyExecutorPoolService.notifyExecutorPool(this.ip, this.port, this.executorType)) { + if(!notifyExecutorPoolService.notifyExecutorPool(this.executorURI, this.executorType)) { System.exit(0); } else { this.status = ExecutorStatus.IDLING; @@ -48,8 +51,7 @@ public abstract class ExecutorBase { } public void getAssignment() { - Task newTask = getAssignmentPort.getAssignment(this.getExecutorType(), this.getIp(), - this.getPort()); + Task newTask = getAssignmentPort.getAssignment(this.getExecutorType(), this.getExecutorURI()); if (newTask != null) { this.executeTask(newTask); } else { diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java index 8bd5bc3..d65412e 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.domain; +package ch.unisg.executorbase.executor.domain; public enum ExecutorStatus { STARTING_UP, diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java index 0b2b305..a8bc0e1 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.domain; +package ch.unisg.executorbase.executor.domain; public enum ExecutorType { ADDITION, ROBOT; diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java index fec330f..0a2164f 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java @@ -1,4 +1,4 @@ -package ch.unisg.executorBase.executor.domain; +package ch.unisg.executorbase.executor.domain; import lombok.Getter; import lombok.Setter; diff --git a/executor-base/src/main/resources/application.properties b/executor-base/src/main/resources/application.properties index 4d360de..3eee96a 100644 --- a/executor-base/src/main/resources/application.properties +++ b/executor-base/src/main/resources/application.properties @@ -1 +1,6 @@ server.port=8081 +roster.url=http://127.0.0.1:8082 +executor-pool.url=http://127.0.0.1:8083 +executor1.url=http://127.0.0.1:8084 +executor2.url=http://127.0.0.1:8085 +task-list.url=http://127.0.0.1:8081 diff --git a/executor-base/src/test/java/ch/unisg/executorBase/Executor1ApplicationTests.java b/executor-base/src/test/java/ch/unisg/executorBase/Executor1ApplicationTests.java deleted file mode 100644 index 6fec034..0000000 --- a/executor-base/src/test/java/ch/unisg/executorBase/Executor1ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package ch.unisg.executorBase; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class executorBaseApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java b/executor1/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java index 1f08545..a0e4f75 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java +++ b/executor1/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java @@ -9,9 +9,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableCommand; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableUseCase; +import ch.unisg.executorbase.executor.domain.ExecutorType; @RestController public class TaskAvailableController { diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java b/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java index 5b011c1..35bbb93 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java +++ b/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java @@ -1,6 +1,6 @@ package ch.unisg.executor1.executor.application.port.out; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.executorbase.executor.domain.ExecutorType; public interface UserToRobotPort { String userToRobot(); diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java b/executor1/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java index d502053..c709fcf 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java +++ b/executor1/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java @@ -3,9 +3,9 @@ package ch.unisg.executor1.executor.application.service; import org.springframework.stereotype.Component; import ch.unisg.executor1.executor.domain.Executor; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; -import ch.unisg.executorBase.executor.domain.ExecutorStatus; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableCommand; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableUseCase; +import ch.unisg.executorbase.executor.domain.ExecutorStatus; import lombok.RequiredArgsConstructor; import javax.transaction.Transactional; @@ -20,7 +20,7 @@ public class TaskAvailableService implements TaskAvailableUseCase { Executor executor = Executor.getExecutor(); - if (executor.getExecutorType() == command.getTaskType() && + if (executor.getExecutorType() == command.getTaskType() && executor.getStatus() == ExecutorStatus.IDLING) { executor.getAssignment(); } diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/domain/Executor.java b/executor1/src/main/java/ch/unisg/executor1/executor/domain/Executor.java index cc11e64..84b576a 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/domain/Executor.java +++ b/executor1/src/main/java/ch/unisg/executor1/executor/domain/Executor.java @@ -10,8 +10,8 @@ import ch.unisg.executor1.executor.adapter.out.UserToRobotAdapter; import ch.unisg.executor1.executor.application.port.out.DeleteUserFromRobotPort; import ch.unisg.executor1.executor.application.port.out.InstructionToRobotPort; import ch.unisg.executor1.executor.application.port.out.UserToRobotPort; -import ch.unisg.executorBase.executor.domain.ExecutorBase; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.executorbase.executor.domain.ExecutorBase; +import ch.unisg.executorbase.executor.domain.ExecutorType; public class Executor extends ExecutorBase { diff --git a/executor2/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java b/executor2/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java index a14b58f..bf53c24 100644 --- a/executor2/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java +++ b/executor2/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java @@ -9,9 +9,9 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; -import ch.unisg.executorBase.executor.domain.ExecutorType; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableCommand; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableUseCase; +import ch.unisg.executorbase.executor.domain.ExecutorType; @RestController public class TaskAvailableController { diff --git a/executor2/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java b/executor2/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java index 6fa918d..49b8e70 100644 --- a/executor2/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java +++ b/executor2/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java @@ -3,9 +3,9 @@ package ch.unisg.executor2.executor.application.service; import org.springframework.stereotype.Component; import ch.unisg.executor2.executor.domain.Executor; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; -import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; -import ch.unisg.executorBase.executor.domain.ExecutorStatus; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableCommand; +import ch.unisg.executorbase.executor.application.port.in.TaskAvailableUseCase; +import ch.unisg.executorbase.executor.domain.ExecutorStatus; import lombok.RequiredArgsConstructor; import javax.transaction.Transactional; diff --git a/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java b/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java index 4d022b5..0708434 100644 --- a/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java +++ b/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java @@ -1,8 +1,9 @@ package ch.unisg.executor2.executor.domain; import java.util.concurrent.TimeUnit; -import ch.unisg.executorBase.executor.domain.ExecutorBase; -import ch.unisg.executorBase.executor.domain.ExecutorType; + +import ch.unisg.executorbase.executor.domain.ExecutorBase; +import ch.unisg.executorbase.executor.domain.ExecutorType; public class Executor extends ExecutorBase { From 3184ab3389e14d20a5a26d11fb6f0b504b45935f Mon Sep 17 00:00:00 2001 From: "julius.lautz" Date: Mon, 1 Nov 2021 22:07:21 +0100 Subject: [PATCH 08/40] cleaned up task list + started implementation of deleteTask --- tapas-tasks/pom.xml | 6 ++ .../in/formats/TaskJsonRepresentation.java | 2 +- .../in/web/CompleteTaskWebController.java | 12 ++- .../in/web/DeleteTaskWebController.java | 15 ++-- .../in/web/TaskAssignedWebController.java | 12 ++- .../tasks/adapter/in/web/TaskMediaType.java | 23 ------ .../out/web/CanTaskBeDeletedWebAdapter.java | 59 +++++++++++++++ .../PublishNewTaskAddedEventWebAdapter.java | 2 +- .../port/in/DeleteTaskCommand.java | 7 +- .../port/out/CanTaskBeDeletedPort.java | 7 ++ .../service/CompleteTaskService.java | 6 +- .../service/DeleteTaskService.java | 11 ++- .../service/TaskAssignedService.java | 2 +- .../tasks/domain/DeleteTaskEvent.java | 11 +++ .../unisg/tapastasks/tasks/domain/Task.java | 35 ++++----- .../tapastasks/tasks/domain/TaskList.java | 75 ++++++++++--------- 16 files changed, 178 insertions(+), 107 deletions(-) delete mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/CanTaskBeDeletedWebAdapter.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/CanTaskBeDeletedPort.java create mode 100644 tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/DeleteTaskEvent.java diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml index 0118cf9..a815cef 100644 --- a/tapas-tasks/pom.xml +++ b/tapas-tasks/pom.xml @@ -75,6 +75,12 @@ org.eclipse.paho.client.mqttv3 1.2.0 + + com.vaadin.external.google + android-json + 0.0.20131108.vaadin1 + compile + diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java index eb89415..ff8158a 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java @@ -88,7 +88,7 @@ final public class TaskJsonRepresentation { this.taskId = task.getTaskId().getValue(); this.taskStatus = task.getTaskStatus().getValue().name(); - this.originalTaskUri = (task.getOriginalTaskUri() == null) ? + this.originalTaskUri = (task. getOriginalTaskUri() == null) ? null : task.getOriginalTaskUri().getValue(); this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue(); diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java index 536b72c..ec2b7b0 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/CompleteTaskWebController.java @@ -1,8 +1,10 @@ package ch.unisg.tapastasks.tasks.adapter.in.web; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; import ch.unisg.tapastasks.tasks.application.port.in.CompleteTaskCommand; import ch.unisg.tapastasks.tasks.application.port.in.CompleteTaskUseCase; 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; @@ -21,7 +23,7 @@ public class CompleteTaskWebController { this.completeTaskUseCase = completeTaskUseCase; } - @PostMapping(path = "/tasks/completeTask", consumes = {TaskMediaType.TASK_MEDIA_TYPE}) + @PostMapping(path = "/tasks/completeTask", consumes = {TaskJsonRepresentation.MEDIA_TYPE}) public ResponseEntity completeTask (@RequestBody Task task){ try { @@ -32,10 +34,12 @@ public class CompleteTaskWebController { Task updateATask = completeTaskUseCase.completeTask(command); HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); - return new ResponseEntity<>(TaskMediaType.serialize(updateATask), responseHeaders, HttpStatus.ACCEPTED); - } catch(ConstraintViolationException e){ + return new ResponseEntity<>(TaskJsonRepresentation.serialize(updateATask), responseHeaders, HttpStatus.ACCEPTED); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } catch (ConstraintViolationException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/DeleteTaskWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/DeleteTaskWebController.java index af721d1..ef79e6a 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/DeleteTaskWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/DeleteTaskWebController.java @@ -1,9 +1,11 @@ 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.DeleteTaskCommand; import ch.unisg.tapastasks.tasks.application.port.in.DeleteTaskUseCase; 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; @@ -23,26 +25,27 @@ public class DeleteTaskWebController { this.deleteClassUseCase = deleteClassUseCase; } - @PostMapping(path="/tasks/deleteTask", consumes = {TaskMediaType.TASK_MEDIA_TYPE}) + @PostMapping(path="/tasks/deleteTask", consumes = {TaskJsonRepresentation.MEDIA_TYPE}) public ResponseEntity deleteTask (@RequestBody Task task){ try { - DeleteTaskCommand command = new DeleteTaskCommand(task.getTaskId()); + DeleteTaskCommand command = new DeleteTaskCommand(task.getTaskId(), task.getOriginalTaskUri()); Optional deleteATask = deleteClassUseCase.deleteTask(command); // Check if the task with the given identifier exists if (deleteATask.isEmpty()) { - // If not, through a 404 Not Found status code throw new ResponseStatusException(HttpStatus.NOT_FOUND); } HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); - return new ResponseEntity<>(TaskMediaType.serialize(deleteATask.get()), responseHeaders, HttpStatus.ACCEPTED); - } catch(ConstraintViolationException e){ + return new ResponseEntity<>(TaskJsonRepresentation.serialize(deleteATask.get()), responseHeaders, HttpStatus.ACCEPTED); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } catch (ConstraintViolationException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskAssignedWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskAssignedWebController.java index 9dfa6a2..b58d159 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskAssignedWebController.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskAssignedWebController.java @@ -1,8 +1,10 @@ package ch.unisg.tapastasks.tasks.adapter.in.web; +import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation; import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedCommand; import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedUseCase; 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; @@ -21,7 +23,7 @@ public class TaskAssignedWebController { this.taskAssignedUseCase = taskAssignedUseCase; } - @PostMapping(path="/tasks/assignTask", consumes= {TaskMediaType.TASK_MEDIA_TYPE}) + @PostMapping(path="/tasks/assignTask", consumes= {TaskJsonRepresentation.MEDIA_TYPE}) public ResponseEntity assignTask(@RequestBody Task task){ try{ TaskAssignedCommand command = new TaskAssignedCommand( @@ -31,10 +33,12 @@ public class TaskAssignedWebController { Task updateATask = taskAssignedUseCase.assignTask(command); HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE); - return new ResponseEntity<>(TaskMediaType.serialize(updateATask), responseHeaders, HttpStatus.ACCEPTED); - } catch (ConstraintViolationException e){ + return new ResponseEntity<>(TaskJsonRepresentation.serialize(updateATask), responseHeaders, HttpStatus.ACCEPTED); + } catch (JsonProcessingException e) { + throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage()); + } catch (ConstraintViolationException e) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java deleted file mode 100644 index d9a0a46..0000000 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java +++ /dev/null @@ -1,23 +0,0 @@ -package ch.unisg.tapastasks.tasks.adapter.in.web; - -import ch.unisg.tapastasks.tasks.domain.Task; -import ch.unisg.tapastasks.tasks.domain.TaskList; -import org.json.JSONObject; - -final public class TaskMediaType { - public static final String TASK_MEDIA_TYPE = "application/json"; - - public static String serialize(Task task) { - JSONObject payload = new JSONObject(); - - payload.put("taskId", task.getTaskId().getValue()); - payload.put("taskName", task.getTaskName().getValue()); - payload.put("taskType", task.getTaskType().getValue()); - payload.put("taskState", task.getTaskState().getValue()); - payload.put("taskListName", TaskList.getTapasTaskList().getTaskListName().getValue()); - payload.put("taskResult", task.getTaskResult().getValue()); - return payload.toString(); - } - - private TaskMediaType() { } -} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/CanTaskBeDeletedWebAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/CanTaskBeDeletedWebAdapter.java new file mode 100644 index 0000000..5061e3d --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/CanTaskBeDeletedWebAdapter.java @@ -0,0 +1,59 @@ +package ch.unisg.tapastasks.tasks.adapter.out.web; + + +import ch.unisg.tapastasks.tasks.application.port.out.CanTaskBeDeletedPort; +import ch.unisg.tapastasks.tasks.domain.DeleteTaskEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +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.HashMap; + +@Component +@Primary +public class CanTaskBeDeletedWebAdapter implements CanTaskBeDeletedPort { + + // Base URI of the service interested in this event + //Todo: Add the right IP address + String server = null; + + @Override + public void canTaskBeDeletedEvent(DeleteTaskEvent event){ + + var values = new HashMap<> () {{ + put("taskId", event.taskId); + put("taskUri", event.taskUri); + }}; + + var objectMapper = new ObjectMapper(); + String requestBody = null; + try { + requestBody = objectMapper.writeValueAsString(values); + } catch (JsonProcessingException e){ + e.printStackTrace(); + } + + //Todo: Question: How do we include the URI from the DeleteTaskEvent? Do we even need it? + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(server+"task")) + .header("Content-Type", "application/task+json") + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + //Todo: The following parameters probably need to be changed to get the right error code + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e){ + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java index d642eca..569b1e9 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java @@ -42,7 +42,7 @@ public class PublishNewTaskAddedEventWebAdapter implements NewTaskAddedEventPort HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(server+"/task")) - .header("Content-Type", "application/json") + .header("Content-Type", "application/task+json") .POST(HttpRequest.BodyPublishers.ofString(requestBody)) .build(); diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/DeleteTaskCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/DeleteTaskCommand.java index 24acbb8..b57c719 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/DeleteTaskCommand.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/DeleteTaskCommand.java @@ -2,6 +2,7 @@ package ch.unisg.tapastasks.tasks.application.port.in; import ch.unisg.tapastasks.common.SelfValidating; import ch.unisg.tapastasks.tasks.domain.Task.TaskId; +import ch.unisg.tapastasks.tasks.domain.Task.OriginalTaskUri; import lombok.Value; import javax.validation.constraints.NotNull; @@ -11,8 +12,12 @@ public class DeleteTaskCommand extends SelfValidating { @NotNull private final TaskId taskId; - public DeleteTaskCommand(TaskId taskId){ + @NotNull + private final OriginalTaskUri taskUri; + + public DeleteTaskCommand(TaskId taskId, OriginalTaskUri taskUri){ this.taskId=taskId; + this.taskUri = taskUri; this.validateSelf(); } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/CanTaskBeDeletedPort.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/CanTaskBeDeletedPort.java new file mode 100644 index 0000000..67bde16 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/CanTaskBeDeletedPort.java @@ -0,0 +1,7 @@ +package ch.unisg.tapastasks.tasks.application.port.out; + +import ch.unisg.tapastasks.tasks.domain.DeleteTaskEvent; + +public interface CanTaskBeDeletedPort { + void canTaskBeDeletedEvent(DeleteTaskEvent event); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java index bade832..0e7f817 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/CompleteTaskService.java @@ -19,17 +19,13 @@ public class CompleteTaskService implements CompleteTaskUseCase { @Override public Task completeTask(CompleteTaskCommand command){ - // TODO Retrieve the task based on ID TaskList taskList = TaskList.getTapasTaskList(); Optional updatedTask = taskList.retrieveTaskById(command.getTaskId()); - // TODO Update the status and result (and save?) Task newTask = updatedTask.get(); newTask.taskResult = new TaskResult(command.getTaskResult().getValue()); - newTask.taskState = new TaskState(Task.State.EXECUTED); + newTask.taskStatus = new TaskStatus(Task.Status.EXECUTED); - - // TODO return the updated task return newTask; } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java index cfebcd6..f865f4c 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java @@ -19,10 +19,15 @@ public class DeleteTaskService implements DeleteTaskUseCase { @Override public Optional deleteTask(DeleteTaskCommand command){ - // TODO check with assignment service if we can delte - TaskList taskList = TaskList.getTapasTaskList(); - return taskList.deleteTaskById(command.getTaskId()); + Optional updatedTask = taskList.retrieveTaskById(command.getTaskId()); + Task newTask = updatedTask.get(); + // TODO: Fill in the right condition into the if-statement and the else-statement + if (/*the task can be deleted*/){ + return taskList.deleteTaskById(command.getTaskId()); + } else { + /*send message back to TaskList that the task cannot be deleted*/ + } } } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/TaskAssignedService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/TaskAssignedService.java index baa6059..6c580e4 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/TaskAssignedService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/TaskAssignedService.java @@ -24,7 +24,7 @@ public class TaskAssignedService implements TaskAssignedUseCase { // update the status to assigned Task updatedTask = task.get(); - updatedTask.taskState = new TaskState(State.ASSIGNED); + updatedTask.taskStatus = new TaskStatus(Status.ASSIGNED); return updatedTask; } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/DeleteTaskEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/DeleteTaskEvent.java new file mode 100644 index 0000000..16e803b --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/DeleteTaskEvent.java @@ -0,0 +1,11 @@ +package ch.unisg.tapastasks.tasks.domain; + +public class DeleteTaskEvent { + public String taskId; + public String taskUri; + + public DeleteTaskEvent(String taskId, String taskUri){ + this.taskId = taskId; + this.taskUri = taskUri; + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java index ebe9d1c..4444beb 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java @@ -21,40 +21,33 @@ public class Task { @Getter private final TaskType taskType; - @Getter - public TaskState taskState; // had to make public for CompleteTaskService + @Getter @Setter + public TaskStatus taskStatus; // had to make public for CompleteTaskService @Getter public TaskResult taskResult; // same as above - // private final OriginalTaskUri originalTaskUri; + @Getter + private final OriginalTaskUri originalTaskUri; - // @Getter @Setter - // private TaskStatus taskStatus; + @Getter @Setter + private ServiceProvider provider; - // @Getter @Setter - // private ServiceProvider provider; + @Getter @Setter + private InputData inputData; - // @Getter @Setter - // private InputData inputData; - - // @Getter @Setter - // private OutputData outputData; + @Getter @Setter + private OutputData outputData; public Task(TaskName taskName, TaskType taskType, OriginalTaskUri taskUri) { - this.taskId = new TaskId(UUID.randomUUID().toString()); - this.taskName = taskName; this.taskType = taskType; - this.taskState = new TaskState(State.OPEN); + this.taskStatus = new TaskStatus(Status.OPEN); this.taskId = new TaskId(UUID.randomUUID().toString()); this.taskResult = new TaskResult(""); - // this.originalTaskUri = taskUri; - - // this.taskStatus = new TaskStatus(Status.OPEN); - - // this.inputData = null; - // this.outputData = null; + this.originalTaskUri = taskUri; + this.inputData = null; + this.outputData = null; } protected static Task createTaskWithNameAndType(TaskName name, TaskType type) { diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java index 7a4e70f..e07bcd8 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java @@ -67,50 +67,51 @@ public class TaskList { } public Optional deleteTaskById(Task.TaskId id) { - for (Task task: listOfTasks.value){ - if(task.getTaskId().getValue().equalsIgnoreCase(id.getValue())){ + for (Task task : listOfTasks.value) { + if (task.getTaskId().getValue().equalsIgnoreCase(id.getValue())) { listOfTasks.value.remove(task); return Optional.of(task); } } return Optional.empty(); - // public Task changeTaskStatusToAssigned(Task.TaskId id, Optional serviceProvider) - // throws TaskNotFoundException { - // return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty()); - // } - - // public Task changeTaskStatusToRunning(Task.TaskId id, Optional serviceProvider) - // throws TaskNotFoundException { - // return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty()); - // } - - // public Task changeTaskStatusToExecuted(Task.TaskId id, Optional serviceProvider, - // Optional outputData) throws TaskNotFoundException { - // return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData); - // } - - // private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional serviceProvider, - // Optional outputData) { - // Optional taskOpt = retrieveTaskById(id); - - // if (taskOpt.isEmpty()) { - // throw new TaskNotFoundException(); - // } - - // Task task = taskOpt.get(); - // task.setTaskStatus(status); - - // if (serviceProvider.isPresent()) { - // task.setProvider(serviceProvider.get()); - // } - - // if (outputData.isPresent()) { - // task.setOutputData(outputData.get()); - // } - - // return task; } + public Task changeTaskStatusToAssigned(Task.TaskId id, Optional serviceProvider) + throws TaskNotFoundException { + return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty()); + } + + public Task changeTaskStatusToRunning(Task.TaskId id, Optional serviceProvider) + throws TaskNotFoundException { + return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty()); + } + + public Task changeTaskStatusToExecuted(Task.TaskId id, Optional serviceProvider, + Optional outputData) throws TaskNotFoundException { + return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData); + } + + private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional serviceProvider, + Optional outputData) { + Optional taskOpt = retrieveTaskById(id); + + if (taskOpt.isEmpty()) { + throw new TaskNotFoundException(); + } + + Task task = taskOpt.get(); + task.setTaskStatus(status); + + if (serviceProvider.isPresent()) { + task.setProvider(serviceProvider.get()); + } + + if (outputData.isPresent()) { + task.setOutputData(outputData.get()); + } + + return task; + } @Value public static class TaskListName { From 7af2b6df66cf177e75c441fbf0abaaae6ad10b10 Mon Sep 17 00:00:00 2001 From: "julius.lautz" Date: Mon, 1 Nov 2021 22:18:00 +0100 Subject: [PATCH 09/40] cleaned up task list + started implementation of deleteTask --- .../tasks/adapter/in/formats/TaskJsonRepresentation.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java index ff8158a..eb89415 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java @@ -88,7 +88,7 @@ final public class TaskJsonRepresentation { this.taskId = task.getTaskId().getValue(); this.taskStatus = task.getTaskStatus().getValue().name(); - this.originalTaskUri = (task. getOriginalTaskUri() == null) ? + this.originalTaskUri = (task.getOriginalTaskUri() == null) ? null : task.getOriginalTaskUri().getValue(); this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue(); From 06172b34cda556ee53b0c7564cf29ce314e83ac4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 2 Nov 2021 20:58:59 +0100 Subject: [PATCH 10/40] code improvements & delete task endpoint --- .../in/web/ApplyForTaskController.java | 5 ++- .../adapter/in/web/DeleteTaskController.java | 35 +++++++++++++++++++ .../adapter/in/web/NewTaskController.java | 6 +++- .../in/web/TaskCompletedController.java | 4 +++ .../in/web/WebControllerExceptionHandler.java | 4 +++ ...llExecutorInExecutorPoolByTypeAdapter.java | 5 +++ .../out/web/PublishNewTaskEventAdapter.java | 4 +++ .../web/PublishTaskAssignedEventAdapter.java | 4 +++ .../web/PublishTaskCompletedEventAdapter.java | 4 +++ .../port/in/DeleteTaskCommand.java | 24 +++++++++++++ .../port/in/DeleteTaskUseCase.java | 5 +++ ...etAllExecutorInExecutorPoolByTypePort.java | 4 +++ .../port/out/NewTaskEventPort.java | 4 +++ .../port/out/TaskAssignedEventPort.java | 4 +++ .../port/out/TaskCompletedEventPort.java | 4 +++ .../service/ApplyForTaskService.java | 5 +++ .../service/DeleteTaskService.java | 27 ++++++++++++++ .../application/service/NewTaskService.java | 12 ++++--- .../service/TaskCompletedService.java | 4 +++ .../assignment/assignment/domain/Roster.java | 34 ++++++++++++++++++ .../unisg/common/valueobject/ExecutorURI.java | 13 ++++--- .../in/web/TaskAvailableController.java | 6 +++- .../web/ExecutionFinishedEventAdapter.java | 6 +++- .../adapter/out/web/GetAssignmentAdapter.java | 7 ++++ .../out/web/NotifyExecutorPoolAdapter.java | 4 +++ .../service/TaskAvailableService.java | 2 +- .../executor/domain/ExecutorBase.java | 34 +++++++++++++----- .../executor/domain/ExecutorStatus.java | 6 ++-- .../executor/domain/ExecutorType.java | 4 +++ 29 files changed, 256 insertions(+), 24 deletions(-) create mode 100644 assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/DeleteTaskController.java create mode 100644 assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskCommand.java create mode 100644 assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskUseCase.java create mode 100644 assignment/src/main/java/ch/unisg/assignment/assignment/application/service/DeleteTaskService.java diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java index c77f6f9..7b8331c 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java @@ -17,6 +17,10 @@ public class ApplyForTaskController { this.applyForTaskUseCase = applyForTaskUseCase; } + /** + * Checks if task is available for the requesting executor. + * @return a task or null if no task found + **/ @PostMapping(path = "/task/apply", consumes = {"application/json"}) public Task applyForTask(@RequestBody ExecutorInfo executorInfo) { @@ -24,6 +28,5 @@ public class ApplyForTaskController { executorInfo.getExecutorURI()); return applyForTaskUseCase.applyForTask(command); - } } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/DeleteTaskController.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/DeleteTaskController.java new file mode 100644 index 0000000..b34e6db --- /dev/null +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/DeleteTaskController.java @@ -0,0 +1,35 @@ +package ch.unisg.assignment.assignment.adapter.in.web; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import ch.unisg.assignment.assignment.application.port.in.DeleteTaskCommand; +import ch.unisg.assignment.assignment.application.port.in.DeleteTaskUseCase; +import ch.unisg.assignment.assignment.domain.Task; + +@RestController +public class DeleteTaskController { + private final DeleteTaskUseCase deleteTaskUseCase; + + public DeleteTaskController(DeleteTaskUseCase deleteTaskUseCase) { + this.deleteTaskUseCase = deleteTaskUseCase; + } + + /** + * Controller to delete a task + * @return 200 OK, 409 Conflict + **/ + @DeleteMapping(path = "/task", consumes = {"application/task+json"}) + public ResponseEntity applyForTask(@RequestBody Task task) { + + DeleteTaskCommand command = new DeleteTaskCommand(task.getTaskID(), task.getTaskType()); + + if (deleteTaskUseCase.deleteTask(command)) { + return new ResponseEntity<>(HttpStatus.OK); + } + return new ResponseEntity<>(HttpStatus.CONFLICT); + } +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/NewTaskController.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/NewTaskController.java index 18bad8f..9faf2ec 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/NewTaskController.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/NewTaskController.java @@ -18,7 +18,11 @@ public class NewTaskController { this.newTaskUseCase = newTaskUseCase; } - @PostMapping(path = "/task", consumes = {"application/json"}) + /** + * Controller which handles the new task event from the tasklist + * @return 201 Create or 409 Conflict + **/ + @PostMapping(path = "/task", consumes = {"application/task+json"}) public ResponseEntity newTaskController(@RequestBody Task task) { NewTaskCommand command = new NewTaskCommand(task.getTaskID(), task.getTaskType()); diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/TaskCompletedController.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/TaskCompletedController.java index cde4c0a..df89c7f 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/TaskCompletedController.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/TaskCompletedController.java @@ -19,6 +19,10 @@ public class TaskCompletedController { this.taskCompletedUseCase = taskCompletedUseCase; } + /** + * Controller which handles the task completed event from executors + * @return 200 OK + **/ @PostMapping(path = "/task/completed", consumes = {"application/json"}) public ResponseEntity addNewTaskTaskToTaskList(@RequestBody Task task) { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java index 99ad2a5..19cce0d 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java @@ -11,6 +11,10 @@ import ch.unisg.common.exception.InvalidExecutorURIException; @ControllerAdvice public class WebControllerExceptionHandler { + /** + * Handles error of type InvalidExecutorURIException + * @return 404 Bad Request + **/ @ExceptionHandler(InvalidExecutorURIException.class) public ResponseEntity handleException(InvalidExecutorURIException e){ diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java index 0a91805..1c02839 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java @@ -24,6 +24,11 @@ public class GetAllExecutorInExecutorPoolByTypeAdapter implements GetAllExecutor @Value("${executor-pool.url}") private String server; + /** + * Requests all executor of the give type from the executor-pool and cheks if there is one + * avaialable of this type. + * @return Whether an executor exist or not + **/ @Override public boolean doesExecutorTypeExist(ExecutorType type) { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java index db3de1c..10638d3 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java @@ -27,6 +27,10 @@ public class PublishNewTaskEventAdapter implements NewTaskEventPort { Logger logger = Logger.getLogger(PublishNewTaskEventAdapter.class.getName()); + /** + * Informs executors about the availability of a new task. + * @return void + **/ @Override public void publishNewTaskEvent(NewTaskEvent event) { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java index 209525e..45a10f3 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java @@ -25,6 +25,10 @@ public class PublishTaskAssignedEventAdapter implements TaskAssignedEventPort { Logger logger = Logger.getLogger(PublishTaskAssignedEventAdapter.class.getName()); + /** + * Informs the task service about the assignment of the task. + * @return void + **/ @Override public void publishTaskAssignedEvent(TaskAssignedEvent event) { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java index 6bd56a0..e9c4944 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java @@ -25,6 +25,10 @@ public class PublishTaskCompletedEventAdapter implements TaskCompletedEventPort Logger logger = Logger.getLogger(PublishTaskCompletedEventAdapter.class.getName()); + /** + * Informs the task service about the completion of the task. + * @return void + **/ @Override public void publishTaskCompleted(TaskCompletedEvent event) { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskCommand.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskCommand.java new file mode 100644 index 0000000..7239acc --- /dev/null +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskCommand.java @@ -0,0 +1,24 @@ +package ch.unisg.assignment.assignment.application.port.in; + +import javax.validation.constraints.NotNull; + +import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.common.validation.SelfValidating; +import lombok.EqualsAndHashCode; +import lombok.Value; + +@Value +@EqualsAndHashCode(callSuper=false) +public class DeleteTaskCommand extends SelfValidating { + @NotNull + private final String taskId; + + @NotNull + private final ExecutorType taskType; + + public DeleteTaskCommand(String taskId, ExecutorType taskType) { + this.taskId = taskId; + this.taskType = taskType; + this.validateSelf(); + } +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskUseCase.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskUseCase.java new file mode 100644 index 0000000..e890e8b --- /dev/null +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskUseCase.java @@ -0,0 +1,5 @@ +package ch.unisg.assignment.assignment.application.port.in; + +public interface DeleteTaskUseCase { + boolean deleteTask(DeleteTaskCommand deleteTaskCommand); +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java index e751727..9f6c824 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java @@ -3,6 +3,10 @@ package ch.unisg.assignment.assignment.application.port.out; import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; public interface GetAllExecutorInExecutorPoolByTypePort { + /** + * Checks if a executor with the given type exist in our executor pool + * @return boolean + **/ boolean doesExecutorTypeExist(ExecutorType type); } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/NewTaskEventPort.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/NewTaskEventPort.java index 909a9ba..243c7f2 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/NewTaskEventPort.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/NewTaskEventPort.java @@ -3,5 +3,9 @@ package ch.unisg.assignment.assignment.application.port.out; import ch.unisg.assignment.assignment.domain.event.NewTaskEvent; public interface NewTaskEventPort { + /** + * Publishes the new task event. + * @return void + **/ void publishNewTaskEvent(NewTaskEvent event); } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskAssignedEventPort.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskAssignedEventPort.java index fefd4a1..5f55ec8 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskAssignedEventPort.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskAssignedEventPort.java @@ -3,5 +3,9 @@ package ch.unisg.assignment.assignment.application.port.out; import ch.unisg.assignment.assignment.domain.event.TaskAssignedEvent; public interface TaskAssignedEventPort { + /** + * Publishes the task assigned event. + * @return void + **/ void publishTaskAssignedEvent(TaskAssignedEvent taskAssignedEvent); } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskCompletedEventPort.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskCompletedEventPort.java index 43a8aa5..83ad179 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskCompletedEventPort.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskCompletedEventPort.java @@ -3,5 +3,9 @@ package ch.unisg.assignment.assignment.application.port.out; import ch.unisg.assignment.assignment.domain.event.TaskCompletedEvent; public interface TaskCompletedEventPort { + /** + * Publishes the task completed event. + * @return void + **/ void publishTaskCompleted(TaskCompletedEvent event); } diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java index 5ba1901..dfb70e0 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java @@ -19,6 +19,11 @@ public class ApplyForTaskService implements ApplyForTaskUseCase { private final TaskAssignedEventPort taskAssignedEventPort; + /** + * Checks if a task is available and assignes it to the executor. If task got assigned a task + * assigned event gets published. + * @return assigned task or null if no task is found + **/ @Override public Task applyForTask(ApplyForTaskCommand command) { Task task = Roster.getInstance().assignTaskToExecutor(command.getTaskType(), diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/DeleteTaskService.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/DeleteTaskService.java new file mode 100644 index 0000000..7d67e4a --- /dev/null +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/DeleteTaskService.java @@ -0,0 +1,27 @@ +package ch.unisg.assignment.assignment.application.service; + +import javax.transaction.Transactional; + +import org.springframework.stereotype.Component; + +import ch.unisg.assignment.assignment.application.port.in.DeleteTaskCommand; +import ch.unisg.assignment.assignment.application.port.in.DeleteTaskUseCase; +import ch.unisg.assignment.assignment.domain.Roster; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Component +@Transactional +public class DeleteTaskService implements DeleteTaskUseCase { + + /** + * Check if task can get deleted + * @return if task can get deleted + **/ + @Override + public boolean deleteTask(DeleteTaskCommand command) { + Roster roster = Roster.getInstance(); + return roster.deleteTask(command.getTaskId(), command.getTaskType()); + } + +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java index 8f60789..d240a4b 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java @@ -21,18 +21,22 @@ public class NewTaskService implements NewTaskUseCase { private final NewTaskEventPort newTaskEventPort; private final GetAllExecutorInExecutorPoolByTypePort getAllExecutorInExecutorPoolByTypePort; + /** + * Checks if we can execute the give task, if yes the task gets added to the task queue and return true. + * If the task can not be executed by an internal or auction house executor, the method return false. + * @return boolean + **/ @Override public boolean addNewTaskToQueue(NewTaskCommand command) { - if (!getAllExecutorInExecutorPoolByTypePort.doesExecutorTypeExist(command.getTaskType())) { - return false; - } + // if (!getAllExecutorInExecutorPoolByTypePort.doesExecutorTypeExist(command.getTaskType())) { + // return false; + // } Task task = new Task(command.getTaskID(), command.getTaskType()); Roster.getInstance().addTaskToQueue(task); - // TODO this event should be in the roster function xyz NewTaskEvent newTaskEvent = new NewTaskEvent(task.getTaskType()); newTaskEventPort.publishNewTaskEvent(newTaskEvent); diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/TaskCompletedService.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/TaskCompletedService.java index c8273ff..7c3e7f6 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/TaskCompletedService.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/TaskCompletedService.java @@ -18,6 +18,10 @@ public class TaskCompletedService implements TaskCompletedUseCase { private final TaskCompletedEventPort taskCompletedEventPort; + /** + * Completes the task in the roster and publishes a task completed event. + * @return void + **/ @Override public void taskCompleted(TaskCompletedCommand command) { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java index 560d7fc..fb259c1 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java +++ b/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java @@ -3,6 +3,8 @@ package ch.unisg.assignment.assignment.domain; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.logging.Level; +import java.util.logging.Logger; import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; import ch.unisg.common.valueobject.ExecutorURI; @@ -11,25 +13,41 @@ public class Roster { private static final Roster roster = new Roster(); + // Queues which hold all the tasks which need to be assigned | Will be replaced by message queue later private HashMap> queues = new HashMap<>(); + // Roster witch holds information about which executor is assigned to a task private HashMap rosterMap = new HashMap<>(); + Logger logger = Logger.getLogger(Roster.class.getName()); + public static Roster getInstance() { return roster; } private Roster() {} + /** + * Adds a task to the task queue. + * @return void + * @see Task + **/ public void addTaskToQueue(Task task) { if (queues.containsKey(task.getTaskType().getValue())) { queues.get(task.getTaskType().getValue()).add(task); } else { queues.put(task.getTaskType().getValue(), new ArrayList<>(Arrays.asList(task))); } + logger.log(Level.INFO, "Added task with id {0} to queue", task.getTaskID()); } + /** + * Checks if a task of this type is in a queue and if so assignes it to the executor. + * @return assigned task or null if no task is found + * @see Task + **/ public Task assignTaskToExecutor(ExecutorType taskType, ExecutorURI executorURI) { + // TODO I don't think we need this if if (!queues.containsKey(taskType.getValue())) { return null; } @@ -45,8 +63,24 @@ public class Roster { return task; } + /** + * Removed a task from the roster after if got completed + * @return void + * @see Task + * @see Roster + **/ public void taskCompleted(String taskID) { rosterMap.remove(taskID); + logger.log(Level.INFO, "Task {0} completed", taskID); + } + + /** + * Deletes a task if it is still in the queue. + * @return Whether the task got deleted or not + **/ + public boolean deleteTask(String taskID, ExecutorType taskType) { + logger.log(Level.INFO, "Try to delete task with id {0}", taskID); + return queues.get(taskType.getValue()).removeIf(task -> task.getTaskID().equalsIgnoreCase(taskID)); } } diff --git a/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java b/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java index fc6b62d..a68a7e0 100644 --- a/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java +++ b/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java @@ -8,11 +8,16 @@ public class ExecutorURI { private String value; public ExecutorURI(String uri) throws InvalidExecutorURIException { - if (uri.equalsIgnoreCase("localhost") || - uri.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$")) { - this.value = uri; - } else { + String ip = uri.split(":")[0]; + int port = Integer.parseInt(uri.split(":")[1]); + // Check if valid ip4 + if (!ip.equalsIgnoreCase("localhost") && + !uri.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$")) { + throw new InvalidExecutorURIException(); + // Check if valid port + } else if (port < 1024 || port > 65535) { throw new InvalidExecutorURIException(); } + this.value = uri; } } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java index 6c1c659..8fda5ac 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/in/web/TaskAvailableController.java @@ -19,7 +19,11 @@ public class TaskAvailableController { this.taskAvailableUseCase = taskAvailableUseCase; } - @GetMapping(path = "/newtask/{taskType}") + /** + * Controller for notification about new events. + * @return 200 OK + **/ + @GetMapping(path = "/newtask/{taskType}", consumes = { "application/json" }) public ResponseEntity retrieveTaskFromTaskList(@PathVariable("taskType") String taskType) { if (ExecutorType.contains(taskType.toUpperCase())) { diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java index 0947e4f..a5ae910 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/ExecutionFinishedEventAdapter.java @@ -21,6 +21,10 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort Logger logger = Logger.getLogger(ExecutionFinishedEventAdapter.class.getName()); + /** + * Publishes the execution finished event + * @return void + **/ @Override public void publishExecutionFinishedEvent(ExecutionFinishedEvent event) { @@ -46,7 +50,7 @@ public class ExecutionFinishedEventAdapter implements ExecutionFinishedEventPort logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } - System.out.println("Finish execution event sent with result:" + event.getResult()); + logger.log(Level.INFO, "Finish execution event sent with result: {}", event.getResult()); } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java index 14976f2..ddcd550 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java @@ -28,6 +28,11 @@ public class GetAssignmentAdapter implements GetAssignmentPort { Logger logger = Logger.getLogger(GetAssignmentAdapter.class.getName()); + /** + * Requests a new task assignment + * @return the assigned task + * @see Task + **/ @Override public Task getAssignment(ExecutorType executorType, ExecutorURI executorURI) { @@ -44,7 +49,9 @@ public class GetAssignmentAdapter implements GetAssignmentPort { .build(); try { + logger.info("Sending getAssignment Request"); HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + logger.log(Level.INFO, "getAssignment request result:\n {}", response.body()); if (response.body().equals("")) { return null; } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java index cad09a9..2dba64f 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/NotifyExecutorPoolAdapter.java @@ -27,6 +27,10 @@ public class NotifyExecutorPoolAdapter implements NotifyExecutorPoolPort { Logger logger = Logger.getLogger(NotifyExecutorPoolAdapter.class.getName()); + /** + * Notifies the executor-pool about the startup of this executor + * @return if the notification was successful + **/ @Override public boolean notifyExecutorPool(ExecutorURI executorURI, ExecutorType executorType) { diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java index c770985..050a807 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/application/service/TaskAvailableService.java @@ -15,6 +15,6 @@ public class TaskAvailableService implements TaskAvailableUseCase { @Override public void newTaskAvailable(TaskAvailableCommand command) { - // Placeholder so spring can create a bean + // Placeholder so spring can create a bean, implementation of this function is inside the executors } } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java index 7644887..f0c5fa9 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java @@ -1,5 +1,7 @@ package ch.unisg.executorbase.executor.domain; +import java.util.logging.Logger; + import ch.unisg.common.exception.InvalidExecutorURIException; import ch.unisg.common.valueobject.ExecutorURI; import ch.unisg.executorbase.executor.adapter.out.web.ExecutionFinishedEventAdapter; @@ -23,25 +25,28 @@ public abstract class ExecutorBase { private ExecutorStatus status; // TODO Violation of the Dependency Inversion Principle?, but we havn't really got a better solutions to send a http request / access a service from a domain model + // TODO I guess we can implement the execution as a service but there still is the problem with the startup request. // TODO I guess we can somehow autowire this but I don't know why it's not working :D private final NotifyExecutorPoolPort notifyExecutorPoolPort = new NotifyExecutorPoolAdapter(); private final NotifyExecutorPoolService notifyExecutorPoolService = new NotifyExecutorPoolService(notifyExecutorPoolPort); private final GetAssignmentPort getAssignmentPort = new GetAssignmentAdapter(); private final ExecutionFinishedEventPort executionFinishedEventPort = new ExecutionFinishedEventAdapter(); - public ExecutorBase(ExecutorType executorType) { - System.out.println("Starting Executor"); + Logger logger = Logger.getLogger(ExecutorBase.class.getName()); + + protected ExecutorBase(ExecutorType executorType) { + logger.info("Starting Executor"); + this.status = ExecutorStatus.STARTING_UP; + this.executorType = executorType; // TODO set this automaticly try { this.executorURI = new ExecutorURI("localhost:8084"); } catch (InvalidExecutorURIException e) { - // Shutdown system if ip or port is not valid + // Shutdown system if the executorURI is not valid System.exit(1); } - this.executorType = executorType; - - this.status = ExecutorStatus.STARTING_UP; + // Notify executor-pool about existence. If executor-pools response is successfull start with getting an assignment, else shut down executor. if(!notifyExecutorPoolService.notifyExecutorPool(this.executorURI, this.executorType)) { System.exit(0); } else { @@ -50,6 +55,10 @@ public abstract class ExecutorBase { } } + /** + * Requests a new task from the task queue + * @return void + **/ public void getAssignment() { Task newTask = getAssignmentPort.getAssignment(this.getExecutorType(), this.getExecutorURI()); if (newTask != null) { @@ -59,19 +68,28 @@ public abstract class ExecutorBase { } } + /** + * Start the execution of a task + * @return void + **/ private void executeTask(Task task) { - System.out.println("Starting execution"); + logger.info("Starting execution"); this.status = ExecutorStatus.EXECUTING; task.setResult(execution()); + // TODO implement logic if execution was not successful executionFinishedEventPort.publishExecutionFinishedEvent( new ExecutionFinishedEvent(task.getTaskID(), task.getResult(), "SUCCESS")); - System.out.println("Finish execution"); + logger.info("Finish execution"); getAssignment(); } + /** + * Implementation of the actual execution method of an executor + * @return the execution result + **/ protected abstract String execution(); } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java index d65412e..1fcf7de 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorStatus.java @@ -1,7 +1,7 @@ package ch.unisg.executorbase.executor.domain; public enum ExecutorStatus { - STARTING_UP, - EXECUTING, - IDLING, + STARTING_UP, // Executor is starting + EXECUTING, // Executor is currently executing a task + IDLING, // Executor has no tasks left and is waiting for new instructions } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java index a8bc0e1..ca9533a 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorType.java @@ -3,6 +3,10 @@ package ch.unisg.executorbase.executor.domain; public enum ExecutorType { ADDITION, ROBOT; + /** + * Checks if the give executor type exists. + * @return Wheter the given executor type exists + **/ public static boolean contains(String test) { for (ExecutorType x : ExecutorType.values()) { From dacb5605d7799aa93899edeb9fefa4fdc1564895 Mon Sep 17 00:00:00 2001 From: rahimiankeanu Date: Tue, 2 Nov 2021 21:21:52 +0100 Subject: [PATCH 11/40] executor 2 change --- .../adapter/out/web/GetAssignmentAdapter.java | 4 ++-- .../executor/domain/ExecutorBase.java | 6 +++--- .../executorBase/executor/domain/Task.java | 6 +++++- .../executor2/executor/domain/Executor.java | 21 +++++++++++++------ 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java index 05852fa..411073b 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/adapter/out/web/GetAssignmentAdapter.java @@ -46,8 +46,8 @@ public class GetAssignmentAdapter implements GetAssignmentPort { if (response.body().equals("")) { return null; } - - return new Task(new JSONObject(response.body()).getString("taskID")); + JSONObject responseBody = new JSONObject(response.body()); + return new Task(responseBody.getString("taskID"), responseBody.getString("input")); } catch (IOException | InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java index c9df1a8..83ab862 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java @@ -61,7 +61,7 @@ public abstract class ExecutorBase { System.out.println("Starting execution"); this.status = ExecutorStatus.EXECUTING; - task.setResult(execution()); + task.setResult(execution(task.getInput())); executionFinishedEventPort.publishExecutionFinishedEvent( new ExecutionFinishedEvent(task.getTaskID(), task.getResult(), "SUCCESS")); @@ -70,6 +70,6 @@ public abstract class ExecutorBase { getAssignment(); } - protected abstract String execution(); - + protected abstract String execution(String... input); + } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java index fec330f..f455dcd 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/Task.java @@ -12,8 +12,12 @@ public class Task { @Setter private String result; - public Task(String taskID) { + @Getter + private String[] input; + + public Task(String taskID, String... input) { this.taskID = taskID; + this.input = input; } } diff --git a/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java b/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java index 4d022b5..f2021b8 100644 --- a/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java +++ b/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java @@ -18,19 +18,28 @@ public class Executor extends ExecutorBase { @Override protected - String execution() { + String execution(String... input) { + + double result = Double.NaN; + int a = Integer.parseInt(input[0]); + int b = Integer.parseInt(input[2]); + String operation = input[1]; - int a = 20; - int b = 20; try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); } - int result = a + b; + if (operation == "+") { + result = a + b; + } else if (operation == "*") { + result = a * b; + } else if (operation == "-") { + result = a - b; + } - return Integer.toString(result); + return Double.toString(result); } -} +} \ No newline at end of file From 5606de7c26befb6a25fbd46d4ece686c4a2642f2 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 4 Nov 2021 17:23:49 +0100 Subject: [PATCH 12/40] Change type of ExecutorURI value to URI --- .../unisg/common/valueobject/ExecutorURI.java | 18 ++++-------------- .../executor/domain/ExecutorBase.java | 8 +------- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java b/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java index a68a7e0..627104e 100644 --- a/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java +++ b/common/src/main/java/ch/unisg/common/valueobject/ExecutorURI.java @@ -1,23 +1,13 @@ package ch.unisg.common.valueobject; -import ch.unisg.common.exception.InvalidExecutorURIException; +import java.net.URI; import lombok.Value; @Value public class ExecutorURI { - private String value; + private URI value; - public ExecutorURI(String uri) throws InvalidExecutorURIException { - String ip = uri.split(":")[0]; - int port = Integer.parseInt(uri.split(":")[1]); - // Check if valid ip4 - if (!ip.equalsIgnoreCase("localhost") && - !uri.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)(\\.(?!$)|$)){4}$")) { - throw new InvalidExecutorURIException(); - // Check if valid port - } else if (port < 1024 || port > 65535) { - throw new InvalidExecutorURIException(); - } - this.value = uri; + public ExecutorURI(String uri) { + this.value = URI.create(uri); } } diff --git a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java index f0c5fa9..dddb89d 100644 --- a/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java +++ b/executor-base/src/main/java/ch/unisg/executorBase/executor/domain/ExecutorBase.java @@ -2,7 +2,6 @@ package ch.unisg.executorbase.executor.domain; import java.util.logging.Logger; -import ch.unisg.common.exception.InvalidExecutorURIException; import ch.unisg.common.valueobject.ExecutorURI; import ch.unisg.executorbase.executor.adapter.out.web.ExecutionFinishedEventAdapter; import ch.unisg.executorbase.executor.adapter.out.web.GetAssignmentAdapter; @@ -39,12 +38,7 @@ public abstract class ExecutorBase { this.status = ExecutorStatus.STARTING_UP; this.executorType = executorType; // TODO set this automaticly - try { - this.executorURI = new ExecutorURI("localhost:8084"); - } catch (InvalidExecutorURIException e) { - // Shutdown system if the executorURI is not valid - System.exit(1); - } + this.executorURI = new ExecutorURI("localhost:8084"); // Notify executor-pool about existence. If executor-pools response is successfull start with getting an assignment, else shut down executor. if(!notifyExecutorPoolService.notifyExecutorPool(this.executorURI, this.executorType)) { From a96c8b2d236d4995c379d8c412526c03579447b9 Mon Sep 17 00:00:00 2001 From: rahimiankeanu Date: Fri, 5 Nov 2021 11:32:22 +0100 Subject: [PATCH 13/40] renaming executors --- {executor1 => executorcomputation}/.gitignore | 0 .../.mvn/wrapper/MavenWrapperDownloader.java | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 {executor1 => executorcomputation}/Dockerfile | 0 {executor1 => executorcomputation}/mvnw | 0 {executor1 => executorcomputation}/mvnw.cmd | 0 {executor2 => executorcomputation}/pom.xml | 2 +- .../executor2/ExecutorcomputationApplication.java | 8 ++++---- .../adapter/in/web/TaskAvailableController.java | 2 +- .../application/service/TaskAvailableService.java | 4 ++-- .../unisg/executor2/executor/domain/Executor.java | 2 +- .../src/main/resources/application.properties | 0 .../ExecutorcomputationApplicationTests.java | 4 ++-- {executor2 => executorrobot}/.gitignore | 0 .../.mvn/wrapper/MavenWrapperDownloader.java | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 {executor2 => executorrobot}/Dockerfile | 0 {executor2 => executorrobot}/mvnw | 0 {executor2 => executorrobot}/mvnw.cmd | 0 {executor1 => executorrobot}/pom.xml | 2 +- .../unisg/executor1/ExecutorrobotApplication.java | 8 ++++---- .../adapter/in/web/TaskAvailableController.java | 2 +- .../adapter/out/DeleteUserFromRobotAdapter.java | 4 ++-- .../adapter/out/InstructionToRobotAdapter.java | 4 ++-- .../executor/adapter/out/UserToRobotAdapter.java | 4 ++-- .../port/out/DeleteUserFromRobotPort.java | 2 +- .../port/out/InstructionToRobotPort.java | 2 +- .../application/port/out/UserToRobotPort.java | 2 +- .../application/service/TaskAvailableService.java | 4 ++-- .../unisg/executor1/executor/domain/Executor.java | 14 +++++++------- .../src/main/resources/application.properties | 0 .../executor1/ExecutorrobotApplicationTests.java | 4 ++-- 34 files changed, 37 insertions(+), 37 deletions(-) rename {executor1 => executorcomputation}/.gitignore (100%) rename {executor1 => executorcomputation}/.mvn/wrapper/MavenWrapperDownloader.java (100%) rename {executor1 => executorcomputation}/.mvn/wrapper/maven-wrapper.jar (100%) rename {executor1 => executorcomputation}/.mvn/wrapper/maven-wrapper.properties (100%) rename {executor1 => executorcomputation}/Dockerfile (100%) rename {executor1 => executorcomputation}/mvnw (100%) rename {executor1 => executorcomputation}/mvnw.cmd (100%) rename {executor2 => executorcomputation}/pom.xml (97%) rename executor2/src/main/java/ch/unisg/executor2/Executor2Application.java => executorcomputation/src/main/java/ch/unisg/executor2/ExecutorcomputationApplication.java (50%) rename {executor2 => executorcomputation}/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java (95%) rename {executor2 => executorcomputation}/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java (85%) rename {executor2 => executorcomputation}/src/main/java/ch/unisg/executor2/executor/domain/Executor.java (93%) rename {executor2 => executorcomputation}/src/main/resources/application.properties (100%) rename executor2/src/test/java/ch/unisg/executor2/Executor2ApplicationTests.java => executorcomputation/src/test/java/ch/unisg/executor2/ExecutorcomputationApplicationTests.java (64%) rename {executor2 => executorrobot}/.gitignore (100%) rename {executor2 => executorrobot}/.mvn/wrapper/MavenWrapperDownloader.java (100%) rename {executor2 => executorrobot}/.mvn/wrapper/maven-wrapper.jar (100%) rename {executor2 => executorrobot}/.mvn/wrapper/maven-wrapper.properties (100%) rename {executor2 => executorrobot}/Dockerfile (100%) rename {executor2 => executorrobot}/mvnw (100%) rename {executor2 => executorrobot}/mvnw.cmd (100%) rename {executor1 => executorrobot}/pom.xml (98%) rename executor1/src/main/java/ch/unisg/executor1/Executor1Application.java => executorrobot/src/main/java/ch/unisg/executor1/ExecutorrobotApplication.java (53%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java (96%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java (89%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java (90%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java (91%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java (59%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java (58%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java (66%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java (87%) rename {executor1 => executorrobot}/src/main/java/ch/unisg/executor1/executor/domain/Executor.java (73%) rename {executor1 => executorrobot}/src/main/resources/application.properties (100%) rename executor1/src/test/java/ch/unisg/executor1/Executor1ApplicationTests.java => executorrobot/src/test/java/ch/unisg/executor1/ExecutorrobotApplicationTests.java (68%) diff --git a/executor1/.gitignore b/executorcomputation/.gitignore similarity index 100% rename from executor1/.gitignore rename to executorcomputation/.gitignore diff --git a/executor1/.mvn/wrapper/MavenWrapperDownloader.java b/executorcomputation/.mvn/wrapper/MavenWrapperDownloader.java similarity index 100% rename from executor1/.mvn/wrapper/MavenWrapperDownloader.java rename to executorcomputation/.mvn/wrapper/MavenWrapperDownloader.java diff --git a/executor1/.mvn/wrapper/maven-wrapper.jar b/executorcomputation/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from executor1/.mvn/wrapper/maven-wrapper.jar rename to executorcomputation/.mvn/wrapper/maven-wrapper.jar diff --git a/executor1/.mvn/wrapper/maven-wrapper.properties b/executorcomputation/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from executor1/.mvn/wrapper/maven-wrapper.properties rename to executorcomputation/.mvn/wrapper/maven-wrapper.properties diff --git a/executor1/Dockerfile b/executorcomputation/Dockerfile similarity index 100% rename from executor1/Dockerfile rename to executorcomputation/Dockerfile diff --git a/executor1/mvnw b/executorcomputation/mvnw similarity index 100% rename from executor1/mvnw rename to executorcomputation/mvnw diff --git a/executor1/mvnw.cmd b/executorcomputation/mvnw.cmd similarity index 100% rename from executor1/mvnw.cmd rename to executorcomputation/mvnw.cmd diff --git a/executor2/pom.xml b/executorcomputation/pom.xml similarity index 97% rename from executor2/pom.xml rename to executorcomputation/pom.xml index 1f970e0..f422c55 100644 --- a/executor2/pom.xml +++ b/executorcomputation/pom.xml @@ -9,7 +9,7 @@ ch.unisg - executor2 + executorcomputation 0.0.1-SNAPSHOT executor2 Demo project for Spring Boot diff --git a/executor2/src/main/java/ch/unisg/executor2/Executor2Application.java b/executorcomputation/src/main/java/ch/unisg/executor2/ExecutorcomputationApplication.java similarity index 50% rename from executor2/src/main/java/ch/unisg/executor2/Executor2Application.java rename to executorcomputation/src/main/java/ch/unisg/executor2/ExecutorcomputationApplication.java index 03edb3d..81975ba 100644 --- a/executor2/src/main/java/ch/unisg/executor2/Executor2Application.java +++ b/executorcomputation/src/main/java/ch/unisg/executor2/ExecutorcomputationApplication.java @@ -1,15 +1,15 @@ -package ch.unisg.executor2; +package ch.unisg.executorcomputation; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import ch.unisg.executor2.executor.domain.Executor; +import ch.unisg.executorcomputation.executor.domain.Executor; @SpringBootApplication -public class Executor2Application { +public class ExecutorcomputationApplication { public static void main(String[] args) { - SpringApplication.run(Executor2Application.class, args); + SpringApplication.run(ExecutorcomputationApplication.class, args); Executor.getExecutor(); } diff --git a/executor2/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java b/executorcomputation/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java similarity index 95% rename from executor2/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java rename to executorcomputation/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java index a14b58f..223c88c 100644 --- a/executor2/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java +++ b/executorcomputation/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java @@ -1,4 +1,4 @@ -package ch.unisg.executor2.executor.adapter.in.web; +package ch.unisg.executorcomputation.executor.adapter.in.web; import java.util.concurrent.CompletableFuture; diff --git a/executor2/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java b/executorcomputation/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java similarity index 85% rename from executor2/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java rename to executorcomputation/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java index 6fa918d..23d9056 100644 --- a/executor2/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java +++ b/executorcomputation/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java @@ -1,8 +1,8 @@ -package ch.unisg.executor2.executor.application.service; +package ch.unisg.executorcomputation.executor.application.service; import org.springframework.stereotype.Component; -import ch.unisg.executor2.executor.domain.Executor; +import ch.unisg.executorcomputation.executor.domain.Executor; import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; import ch.unisg.executorBase.executor.domain.ExecutorStatus; diff --git a/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java b/executorcomputation/src/main/java/ch/unisg/executor2/executor/domain/Executor.java similarity index 93% rename from executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java rename to executorcomputation/src/main/java/ch/unisg/executor2/executor/domain/Executor.java index 4d022b5..c6f2a2b 100644 --- a/executor2/src/main/java/ch/unisg/executor2/executor/domain/Executor.java +++ b/executorcomputation/src/main/java/ch/unisg/executor2/executor/domain/Executor.java @@ -1,4 +1,4 @@ -package ch.unisg.executor2.executor.domain; +package ch.unisg.executorcomputation.executor.domain; import java.util.concurrent.TimeUnit; import ch.unisg.executorBase.executor.domain.ExecutorBase; diff --git a/executor2/src/main/resources/application.properties b/executorcomputation/src/main/resources/application.properties similarity index 100% rename from executor2/src/main/resources/application.properties rename to executorcomputation/src/main/resources/application.properties diff --git a/executor2/src/test/java/ch/unisg/executor2/Executor2ApplicationTests.java b/executorcomputation/src/test/java/ch/unisg/executor2/ExecutorcomputationApplicationTests.java similarity index 64% rename from executor2/src/test/java/ch/unisg/executor2/Executor2ApplicationTests.java rename to executorcomputation/src/test/java/ch/unisg/executor2/ExecutorcomputationApplicationTests.java index 5724a1c..f17d100 100644 --- a/executor2/src/test/java/ch/unisg/executor2/Executor2ApplicationTests.java +++ b/executorcomputation/src/test/java/ch/unisg/executor2/ExecutorcomputationApplicationTests.java @@ -1,10 +1,10 @@ -package ch.unisg.executor2; +package ch.unisg.executorcomputation; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class Executor2ApplicationTests { +class ExecutorcomputationApplicationTests { @Test void contextLoads() { diff --git a/executor2/.gitignore b/executorrobot/.gitignore similarity index 100% rename from executor2/.gitignore rename to executorrobot/.gitignore diff --git a/executor2/.mvn/wrapper/MavenWrapperDownloader.java b/executorrobot/.mvn/wrapper/MavenWrapperDownloader.java similarity index 100% rename from executor2/.mvn/wrapper/MavenWrapperDownloader.java rename to executorrobot/.mvn/wrapper/MavenWrapperDownloader.java diff --git a/executor2/.mvn/wrapper/maven-wrapper.jar b/executorrobot/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from executor2/.mvn/wrapper/maven-wrapper.jar rename to executorrobot/.mvn/wrapper/maven-wrapper.jar diff --git a/executor2/.mvn/wrapper/maven-wrapper.properties b/executorrobot/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from executor2/.mvn/wrapper/maven-wrapper.properties rename to executorrobot/.mvn/wrapper/maven-wrapper.properties diff --git a/executor2/Dockerfile b/executorrobot/Dockerfile similarity index 100% rename from executor2/Dockerfile rename to executorrobot/Dockerfile diff --git a/executor2/mvnw b/executorrobot/mvnw similarity index 100% rename from executor2/mvnw rename to executorrobot/mvnw diff --git a/executor2/mvnw.cmd b/executorrobot/mvnw.cmd similarity index 100% rename from executor2/mvnw.cmd rename to executorrobot/mvnw.cmd diff --git a/executor1/pom.xml b/executorrobot/pom.xml similarity index 98% rename from executor1/pom.xml rename to executorrobot/pom.xml index 8a5b9e3..f5348d4 100644 --- a/executor1/pom.xml +++ b/executorrobot/pom.xml @@ -9,7 +9,7 @@ ch.unisg - executor1 + executorrobot 0.0.1-SNAPSHOT executor1 Demo project for Spring Boot diff --git a/executor1/src/main/java/ch/unisg/executor1/Executor1Application.java b/executorrobot/src/main/java/ch/unisg/executor1/ExecutorrobotApplication.java similarity index 53% rename from executor1/src/main/java/ch/unisg/executor1/Executor1Application.java rename to executorrobot/src/main/java/ch/unisg/executor1/ExecutorrobotApplication.java index dfb8d8c..fcee5ee 100644 --- a/executor1/src/main/java/ch/unisg/executor1/Executor1Application.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/ExecutorrobotApplication.java @@ -1,15 +1,15 @@ -package ch.unisg.executor1; +package ch.unisg.executorrobot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import ch.unisg.executor1.executor.domain.Executor; +import ch.unisg.executorrobot.executor.domain.Executor; @SpringBootApplication -public class Executor1Application { +public class ExecutorrobotApplication { public static void main(String[] args) { - SpringApplication.run(Executor1Application.class, args); + SpringApplication.run(ExecutorrobotApplication.class, args); Executor.getExecutor(); } diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java similarity index 96% rename from executor1/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java index 1f08545..c5348df 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java @@ -1,4 +1,4 @@ -package ch.unisg.executor1.executor.adapter.in.web; +package ch.unisg.executorrobot.executor.adapter.in.web; import java.util.concurrent.CompletableFuture; diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java similarity index 89% rename from executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java index 94c2309..157bc3e 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.executor1.executor.adapter.out; +package ch.unisg.executorrobot.executor.adapter.out; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -8,7 +8,7 @@ import java.net.http.HttpResponse; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; -import ch.unisg.executor1.executor.application.port.out.DeleteUserFromRobotPort; +import ch.unisg.executorrobot.executor.application.port.out.DeleteUserFromRobotPort; @Component @Primary diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java similarity index 90% rename from executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java index f8b7012..c7507e4 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.executor1.executor.adapter.out; +package ch.unisg.executorrobot.executor.adapter.out; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -8,7 +8,7 @@ import java.net.http.HttpResponse; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; -import ch.unisg.executor1.executor.application.port.out.InstructionToRobotPort; +import ch.unisg.executorrobot.executor.application.port.out.InstructionToRobotPort; @Component @Primary diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java similarity index 91% rename from executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java index f874892..92ca8c1 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.executor1.executor.adapter.out; +package ch.unisg.executorrobot.executor.adapter.out; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -8,7 +8,7 @@ import java.net.http.HttpResponse; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; -import ch.unisg.executor1.executor.application.port.out.UserToRobotPort; +import ch.unisg.executorrobot.executor.application.port.out.UserToRobotPort; @Component @Primary diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java similarity index 59% rename from executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java index fc6f5d7..2411353 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java @@ -1,4 +1,4 @@ -package ch.unisg.executor1.executor.application.port.out; +package ch.unisg.executorrobot.executor.application.port.out; public interface DeleteUserFromRobotPort { boolean deleteUserFromRobot(String key); diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java similarity index 58% rename from executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java index bbf4034..97985b0 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java @@ -1,4 +1,4 @@ -package ch.unisg.executor1.executor.application.port.out; +package ch.unisg.executorrobot.executor.application.port.out; public interface InstructionToRobotPort { boolean instructionToRobot(String key); diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java similarity index 66% rename from executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java index 5b011c1..b5ab55b 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java @@ -1,4 +1,4 @@ -package ch.unisg.executor1.executor.application.port.out; +package ch.unisg.executorrobot.executor.application.port.out; import ch.unisg.executorBase.executor.domain.ExecutorType; diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java similarity index 87% rename from executor1/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java index d502053..ce1705a 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java @@ -1,8 +1,8 @@ -package ch.unisg.executor1.executor.application.service; +package ch.unisg.executorrobot.executor.application.service; import org.springframework.stereotype.Component; -import ch.unisg.executor1.executor.domain.Executor; +import ch.unisg.executorrobot.executor.domain.Executor; import ch.unisg.executorBase.executor.application.port.in.TaskAvailableCommand; import ch.unisg.executorBase.executor.application.port.in.TaskAvailableUseCase; import ch.unisg.executorBase.executor.domain.ExecutorStatus; diff --git a/executor1/src/main/java/ch/unisg/executor1/executor/domain/Executor.java b/executorrobot/src/main/java/ch/unisg/executor1/executor/domain/Executor.java similarity index 73% rename from executor1/src/main/java/ch/unisg/executor1/executor/domain/Executor.java rename to executorrobot/src/main/java/ch/unisg/executor1/executor/domain/Executor.java index cc11e64..e23dca5 100644 --- a/executor1/src/main/java/ch/unisg/executor1/executor/domain/Executor.java +++ b/executorrobot/src/main/java/ch/unisg/executor1/executor/domain/Executor.java @@ -1,15 +1,15 @@ -package ch.unisg.executor1.executor.domain; +package ch.unisg.executorrobot.executor.domain; import java.net.http.HttpClient; import java.net.http.HttpResponse; import java.util.concurrent.TimeUnit; -import ch.unisg.executor1.executor.adapter.out.DeleteUserFromRobotAdapter; -import ch.unisg.executor1.executor.adapter.out.InstructionToRobotAdapter; -import ch.unisg.executor1.executor.adapter.out.UserToRobotAdapter; -import ch.unisg.executor1.executor.application.port.out.DeleteUserFromRobotPort; -import ch.unisg.executor1.executor.application.port.out.InstructionToRobotPort; -import ch.unisg.executor1.executor.application.port.out.UserToRobotPort; +import ch.unisg.executorrobot.executor.adapter.out.DeleteUserFromRobotAdapter; +import ch.unisg.executorrobot.executor.adapter.out.InstructionToRobotAdapter; +import ch.unisg.executorrobot.executor.adapter.out.UserToRobotAdapter; +import ch.unisg.executorrobot.executor.application.port.out.DeleteUserFromRobotPort; +import ch.unisg.executorrobot.executor.application.port.out.InstructionToRobotPort; +import ch.unisg.executorrobot.executor.application.port.out.UserToRobotPort; import ch.unisg.executorBase.executor.domain.ExecutorBase; import ch.unisg.executorBase.executor.domain.ExecutorType; diff --git a/executor1/src/main/resources/application.properties b/executorrobot/src/main/resources/application.properties similarity index 100% rename from executor1/src/main/resources/application.properties rename to executorrobot/src/main/resources/application.properties diff --git a/executor1/src/test/java/ch/unisg/executor1/Executor1ApplicationTests.java b/executorrobot/src/test/java/ch/unisg/executor1/ExecutorrobotApplicationTests.java similarity index 68% rename from executor1/src/test/java/ch/unisg/executor1/Executor1ApplicationTests.java rename to executorrobot/src/test/java/ch/unisg/executor1/ExecutorrobotApplicationTests.java index 889c9cd..82f67a8 100644 --- a/executor1/src/test/java/ch/unisg/executor1/Executor1ApplicationTests.java +++ b/executorrobot/src/test/java/ch/unisg/executor1/ExecutorrobotApplicationTests.java @@ -1,10 +1,10 @@ -package ch.unisg.executor1; +package ch.unisg.executorrobot; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class Executor1ApplicationTests { +class ExecutorrobotApplicationTests { @Test void contextLoads() { From e0e54f9350309b97a3af8c7cdff243f0cfd8cfb8 Mon Sep 17 00:00:00 2001 From: rahimiankeanu Date: Sun, 7 Nov 2021 23:35:24 +0100 Subject: [PATCH 14/40] Implemented RemovedEventListener... --- .../ExecutorRemovedEventListenerHttpAdapter.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java index 53811f9..fcf9b52 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/http/ExecutorRemovedEventListenerHttpAdapter.java @@ -1,6 +1,14 @@ package ch.unisg.tapas.auctionhouse.adapter.in.messaging.http; +import ch.unisg.tapas.auctionhouse.application.handler.ExecutorRemovedHandler; +import ch.unisg.tapas.auctionhouse.application.port.in.ExecutorRemovedEvent; +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; /** @@ -12,5 +20,13 @@ public class ExecutorRemovedEventListenerHttpAdapter { // TODO: add annotations for request method, request URI, etc. public void handleExecutorRemovedEvent(@PathVariable("executorId") String executorId) { // TODO: implement logic + + ExecutorRemovedEvent executorRemovedEvent = new ExecutorRemovedEvent( + new ExecutorRegistry.ExecutorIdentifier(executorId) + ); + + ExecutorRemovedHandler newExecutorHandler = new ExecutorRemovedHandler(); + newExecutorHandler.handleExecutorRemovedEvent(executorRemovedEvent); + } } From 5c445a2f667d2969d86a6e25f5c4812dbf2f513b Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 8 Nov 2021 14:23:27 +0100 Subject: [PATCH 15/40] rename assignment to roster & added executor registry to roster --- .../port/in/ApplyForTaskUseCase.java | 7 -- {assignment => roster}/.gitignore | 0 .../.mvn/wrapper/MavenWrapperDownloader.java | 0 .../.mvn/wrapper/maven-wrapper.jar | Bin .../.mvn/wrapper/maven-wrapper.properties | 0 {assignment => roster}/Dockerfile | 0 {assignment => roster}/mvnw | 0 {assignment => roster}/mvnw.cmd | 0 {assignment => roster}/pom.xml | 6 ++ .../ch/unisg/roster/RosterApplication.java | 6 +- .../common/clients/TapasMqttClient.java | 94 ++++++++++++++++++ .../mqtt/AuctionEventMqttListener.java | 11 ++ .../mqtt/AuctionEventsMqttDispatcher.java | 52 ++++++++++ ...ExecutorAddedEventListenerMqttAdapter.java | 44 ++++++++ ...ecutorRemovedEventListenerMqttAdapter.java | 41 ++++++++ .../in/web/ApplyForTaskController.java | 10 +- .../adapter/in/web/DeleteTaskController.java | 8 +- .../adapter/in/web/NewTaskController.java | 8 +- .../in/web/TaskCompletedController.java | 8 +- .../in/web/WebControllerExceptionHandler.java | 2 +- ...llExecutorInExecutorPoolByTypeAdapter.java | 6 +- .../out/web/PublishNewTaskEventAdapter.java | 6 +- .../web/PublishTaskAssignedEventAdapter.java | 6 +- .../web/PublishTaskCompletedEventAdapter.java | 6 +- .../handler/ExecutorAddedHandler.java | 16 +++ .../handler/ExecutorRemovedHandler.java | 19 ++++ .../port/in/ApplyForTaskCommand.java | 4 +- .../port/in/ApplyForTaskUseCase.java | 7 ++ .../port/in/DeleteTaskCommand.java | 4 +- .../port/in/DeleteTaskUseCase.java | 2 +- .../port/in/ExecutorAddedEvent.java | 33 ++++++ .../port/in/ExecutorAddedEventHandler.java | 6 ++ .../port/in/ExecutorRemovedEvent.java | 27 +++++ .../port/in/ExecutorRemovedEventHandler.java | 6 ++ .../application/port/in/NewTaskCommand.java | 4 +- .../application/port/in/NewTaskUseCase.java | 2 +- .../port/in/TaskCompletedCommand.java | 2 +- .../port/in/TaskCompletedUseCase.java | 2 +- ...etAllExecutorInExecutorPoolByTypePort.java | 4 +- .../port/out/NewTaskEventPort.java | 4 +- .../port/out/TaskAssignedEventPort.java | 4 +- .../port/out/TaskCompletedEventPort.java | 4 +- .../service/ApplyForTaskService.java | 14 +-- .../service/DeleteTaskService.java | 8 +- .../application/service/NewTaskService.java | 24 ++--- .../service/TaskCompletedService.java | 12 +-- .../roster/roster}/domain/ExecutorInfo.java | 4 +- .../roster/domain/ExecutorRegistry.java | 92 +++++++++++++++++ .../unisg/roster/roster}/domain/Roster.java | 4 +- .../roster/roster}/domain/RosterItem.java | 2 +- .../ch/unisg/roster/roster}/domain/Task.java | 4 +- .../roster}/domain/event/NewTaskEvent.java | 4 +- .../domain/event/TaskAssignedEvent.java | 2 +- .../domain/event/TaskCompletedEvent.java | 2 +- .../domain/valueobject/ExecutorType.java | 2 +- .../src/main/resources/application.properties | 0 .../unisg/roster/RosterApplicationTests.java | 4 +- 57 files changed, 548 insertions(+), 101 deletions(-) delete mode 100644 assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskUseCase.java rename {assignment => roster}/.gitignore (100%) rename {assignment => roster}/.mvn/wrapper/MavenWrapperDownloader.java (100%) rename {assignment => roster}/.mvn/wrapper/maven-wrapper.jar (100%) rename {assignment => roster}/.mvn/wrapper/maven-wrapper.properties (100%) rename {assignment => roster}/Dockerfile (100%) rename {assignment => roster}/mvnw (100%) rename {assignment => roster}/mvnw.cmd (100%) rename {assignment => roster}/pom.xml (94%) rename assignment/src/main/java/ch/unisg/assignment/AssignmentApplication.java => roster/src/main/java/ch/unisg/roster/RosterApplication.java (60%) create mode 100644 roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventMqttListener.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/in/web/ApplyForTaskController.java (73%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/in/web/DeleteTaskController.java (79%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/in/web/NewTaskController.java (81%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/in/web/TaskCompletedController.java (79%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/in/web/WebControllerExceptionHandler.java (93%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java (89%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/out/web/PublishNewTaskEventAdapter.java (91%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/out/web/PublishTaskAssignedEventAdapter.java (88%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/adapter/out/web/PublishTaskCompletedEventAdapter.java (89%) create mode 100644 roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorAddedHandler.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorRemovedHandler.java rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/in/ApplyForTaskCommand.java (82%) create mode 100644 roster/src/main/java/ch/unisg/roster/roster/application/port/in/ApplyForTaskUseCase.java rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/in/DeleteTaskCommand.java (80%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/in/DeleteTaskUseCase.java (62%) create mode 100644 roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEvent.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEventHandler.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEvent.java create mode 100644 roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEventHandler.java rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/in/NewTaskCommand.java (80%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/in/NewTaskUseCase.java (62%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/in/TaskCompletedCommand.java (91%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/in/TaskCompletedUseCase.java (64%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java (63%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/out/NewTaskEventPort.java (56%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/out/TaskAssignedEventPort.java (60%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/port/out/TaskCompletedEventPort.java (58%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/service/ApplyForTaskService.java (65%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/service/DeleteTaskService.java (67%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/service/NewTaskService.java (53%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/application/service/TaskCompletedService.java (63%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/ExecutorInfo.java (67%) create mode 100644 roster/src/main/java/ch/unisg/roster/roster/domain/ExecutorRegistry.java rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/Roster.java (95%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/RosterItem.java (90%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/Task.java (82%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/event/NewTaskEvent.java (56%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/event/TaskAssignedEvent.java (74%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/event/TaskCompletedEvent.java (85%) rename {assignment/src/main/java/ch/unisg/assignment/assignment => roster/src/main/java/ch/unisg/roster/roster}/domain/valueobject/ExecutorType.java (74%) rename {assignment => roster}/src/main/resources/application.properties (100%) rename assignment/src/test/java/ch/unisg/assignment/AssignmentApplicationTests.java => roster/src/test/java/ch/unisg/roster/RosterApplicationTests.java (70%) diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskUseCase.java b/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskUseCase.java deleted file mode 100644 index 1e7180a..0000000 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package ch.unisg.assignment.assignment.application.port.in; - -import ch.unisg.assignment.assignment.domain.Task; - -public interface ApplyForTaskUseCase { - Task applyForTask(ApplyForTaskCommand applyForTaskCommand); -} diff --git a/assignment/.gitignore b/roster/.gitignore similarity index 100% rename from assignment/.gitignore rename to roster/.gitignore diff --git a/assignment/.mvn/wrapper/MavenWrapperDownloader.java b/roster/.mvn/wrapper/MavenWrapperDownloader.java similarity index 100% rename from assignment/.mvn/wrapper/MavenWrapperDownloader.java rename to roster/.mvn/wrapper/MavenWrapperDownloader.java diff --git a/assignment/.mvn/wrapper/maven-wrapper.jar b/roster/.mvn/wrapper/maven-wrapper.jar similarity index 100% rename from assignment/.mvn/wrapper/maven-wrapper.jar rename to roster/.mvn/wrapper/maven-wrapper.jar diff --git a/assignment/.mvn/wrapper/maven-wrapper.properties b/roster/.mvn/wrapper/maven-wrapper.properties similarity index 100% rename from assignment/.mvn/wrapper/maven-wrapper.properties rename to roster/.mvn/wrapper/maven-wrapper.properties diff --git a/assignment/Dockerfile b/roster/Dockerfile similarity index 100% rename from assignment/Dockerfile rename to roster/Dockerfile diff --git a/assignment/mvnw b/roster/mvnw similarity index 100% rename from assignment/mvnw rename to roster/mvnw diff --git a/assignment/mvnw.cmd b/roster/mvnw.cmd similarity index 100% rename from assignment/mvnw.cmd rename to roster/mvnw.cmd diff --git a/assignment/pom.xml b/roster/pom.xml similarity index 94% rename from assignment/pom.xml rename to roster/pom.xml index b4650de..f51bff7 100644 --- a/assignment/pom.xml +++ b/roster/pom.xml @@ -56,6 +56,12 @@ 1.2 + + org.eclipse.paho + org.eclipse.paho.client.mqttv3 + 1.2.5 + + org.json json diff --git a/assignment/src/main/java/ch/unisg/assignment/AssignmentApplication.java b/roster/src/main/java/ch/unisg/roster/RosterApplication.java similarity index 60% rename from assignment/src/main/java/ch/unisg/assignment/AssignmentApplication.java rename to roster/src/main/java/ch/unisg/roster/RosterApplication.java index 30d7782..dd57a5d 100644 --- a/assignment/src/main/java/ch/unisg/assignment/AssignmentApplication.java +++ b/roster/src/main/java/ch/unisg/roster/RosterApplication.java @@ -1,13 +1,13 @@ -package ch.unisg.assignment; +package ch.unisg.roster; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class AssignmentApplication { +public class RosterApplication { public static void main(String[] args) { - SpringApplication.run(AssignmentApplication.class, args); + SpringApplication.run(RosterApplication.class, args); } } diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java new file mode 100644 index 0000000..8b5411b --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/common/clients/TapasMqttClient.java @@ -0,0 +1,94 @@ +package ch.unisg.roster.roster.adapter.common.clients; + +import ch.unisg.roster.roster.adapter.in.messaging.mqtt.AuctionEventsMqttDispatcher; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.*; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; + +import java.nio.charset.StandardCharsets; +import java.util.UUID; + +/** + * MQTT client for your TAPAS application. This class is defined as a singleton, but it does not have + * to be this way. This class is only provided as an example to help you bootstrap your project. + * You are welcomed to change this class as you see fit. + */ +public class TapasMqttClient { + private static final Logger LOGGER = LogManager.getLogger(TapasMqttClient.class); + + private static TapasMqttClient tapasClient = null; + + private MqttClient mqttClient; + private final String mqttClientId; + private final String brokerAddress; + + private final MessageReceivedCallback messageReceivedCallback; + + private final AuctionEventsMqttDispatcher dispatcher; + + private TapasMqttClient(String brokerAddress, AuctionEventsMqttDispatcher dispatcher) { + this.mqttClientId = UUID.randomUUID().toString(); + this.brokerAddress = brokerAddress; + + this.messageReceivedCallback = new MessageReceivedCallback(); + + this.dispatcher = dispatcher; + } + + public static synchronized TapasMqttClient getInstance(String brokerAddress, + AuctionEventsMqttDispatcher dispatcher) { + + if (tapasClient == null) { + tapasClient = new TapasMqttClient(brokerAddress, dispatcher); + } + + return tapasClient; + } + + public void startReceivingMessages() throws MqttException { + mqttClient = new org.eclipse.paho.client.mqttv3.MqttClient(brokerAddress, mqttClientId, new MemoryPersistence()); + mqttClient.connect(); + mqttClient.setCallback(messageReceivedCallback); + + subscribeToAllTopics(); + } + + public void stopReceivingMessages() throws MqttException { + mqttClient.disconnect(); + } + + private void subscribeToAllTopics() throws MqttException { + for (String topic : dispatcher.getAllTopics()) { + subscribeToTopic(topic); + } + } + + private void subscribeToTopic(String topic) throws MqttException { + mqttClient.subscribe(topic); + } + + private void publishMessage(String topic, String payload) throws MqttException { + MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8)); + mqttClient.publish(topic, message); + } + + private class MessageReceivedCallback implements MqttCallback { + + @Override + public void connectionLost(Throwable cause) { } + + @Override + public void messageArrived(String topic, MqttMessage message) { + LOGGER.info("Received new MQTT message for topic " + topic + ": " + + new String(message.getPayload())); + + if (topic != null && !topic.isEmpty()) { + dispatcher.dispatchEvent(topic, message); + } + } + + @Override + public void deliveryComplete(IMqttDeliveryToken token) { } + } +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventMqttListener.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventMqttListener.java new file mode 100644 index 0000000..6eb109f --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventMqttListener.java @@ -0,0 +1,11 @@ +package ch.unisg.roster.roster.adapter.in.messaging.mqtt; + +import org.eclipse.paho.client.mqttv3.MqttMessage; + +/** + * Abstract MQTT listener for auction-related events + */ +public abstract class AuctionEventMqttListener { + + public abstract boolean handleEvent(MqttMessage message); +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java new file mode 100644 index 0000000..d19c803 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/AuctionEventsMqttDispatcher.java @@ -0,0 +1,52 @@ +package ch.unisg.roster.roster.adapter.in.messaging.mqtt; + +import org.eclipse.paho.client.mqttv3.*; + +import java.util.Hashtable; +import java.util.Map; +import java.util.Set; + +/** + * Dispatches MQTT messages for known topics to associated event listeners. Used in conjunction with + * {@link ch.unisg.tapas.auctionhouse.adapter.common.clients.TapasMqttClient}. + * + * This is where you would define MQTT topics and map them to event listeners (see + * {@link AuctionEventsMqttDispatcher#initRouter()}). + * + * This class is only provided as an example to help you bootstrap the project. You are welcomed to + * change this class as you see fit. + */ +public class AuctionEventsMqttDispatcher { + private final Map router; + + public AuctionEventsMqttDispatcher() { + this.router = new Hashtable<>(); + initRouter(); + } + + // TODO: Register here your topics and event listener adapters + private void initRouter() { + router.put("ch/unisg/tapas-group-tutors/executors/added", new ExecutorAddedEventListenerMqttAdapter()); + router.put("ch/unisg/tapas-group-tutors/executors/removed", new ExecutorRemovedEventListenerMqttAdapter()); + } + + /** + * Returns all topics registered with this dispatcher. + * + * @return the set of registered topics + */ + public Set getAllTopics() { + return router.keySet(); + } + + /** + * Dispatches an event received via MQTT for a given topic. + * + * @param topic the topic for which the MQTT message was received + * @param message the received MQTT message + */ + public void dispatchEvent(String topic, MqttMessage message) { + AuctionEventMqttListener listener = router.get(topic); + listener.handleEvent(message); + } +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java new file mode 100644 index 0000000..dd9257e --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorAddedEventListenerMqttAdapter.java @@ -0,0 +1,44 @@ +package ch.unisg.roster.roster.adapter.in.messaging.mqtt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.roster.roster.application.handler.ExecutorAddedHandler; +import ch.unisg.roster.roster.application.port.in.ExecutorAddedEvent; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; + +public class ExecutorAddedEventListenerMqttAdapter extends AuctionEventMqttListener { + private static final Logger LOGGER = LogManager.getLogger(ExecutorAddedEventListenerMqttAdapter.class); + + @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("executorURI").asText(); + + ExecutorAddedEvent executorAddedEvent = new ExecutorAddedEvent( + new ExecutorURI(executorId), + new ExecutorType(taskType) + ); + + ExecutorAddedHandler newExecutorHandler = new ExecutorAddedHandler(); + newExecutorHandler.handleNewExecutorEvent(executorAddedEvent); + } catch (JsonProcessingException | NullPointerException e) { + LOGGER.error(e.getMessage(), e); + return false; + } + + return true; + } +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java new file mode 100644 index 0000000..d7b5067 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/messaging/mqtt/ExecutorRemovedEventListenerMqttAdapter.java @@ -0,0 +1,41 @@ +package ch.unisg.roster.roster.adapter.in.messaging.mqtt; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.paho.client.mqttv3.MqttMessage; + +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.roster.roster.application.handler.ExecutorRemovedHandler; +import ch.unisg.roster.roster.application.port.in.ExecutorRemovedEvent; + +public class ExecutorRemovedEventListenerMqttAdapter extends AuctionEventMqttListener { + private static final Logger LOGGER = LogManager.getLogger(ExecutorRemovedEventListenerMqttAdapter.class); + + @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 executorId = data.get("executorURI").asText(); + + ExecutorRemovedEvent executorRemovedEvent = new ExecutorRemovedEvent( + new ExecutorURI(executorId)); + + ExecutorRemovedHandler executorRemovedHandler = new ExecutorRemovedHandler(); + executorRemovedHandler.handleExecutorRemovedEvent(executorRemovedEvent); + + } catch (JsonProcessingException | NullPointerException e) { + LOGGER.error(e.getMessage(), e); + return false; + } + + return true; + } +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/ApplyForTaskController.java similarity index 73% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/ApplyForTaskController.java index 7b8331c..28170f0 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/ApplyForTaskController.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/ApplyForTaskController.java @@ -1,13 +1,13 @@ -package ch.unisg.assignment.assignment.adapter.in.web; +package ch.unisg.roster.roster.adapter.in.web; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import ch.unisg.assignment.assignment.application.port.in.ApplyForTaskCommand; -import ch.unisg.assignment.assignment.application.port.in.ApplyForTaskUseCase; -import ch.unisg.assignment.assignment.domain.ExecutorInfo; -import ch.unisg.assignment.assignment.domain.Task; +import ch.unisg.roster.roster.application.port.in.ApplyForTaskCommand; +import ch.unisg.roster.roster.application.port.in.ApplyForTaskUseCase; +import ch.unisg.roster.roster.domain.ExecutorInfo; +import ch.unisg.roster.roster.domain.Task; @RestController public class ApplyForTaskController { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/DeleteTaskController.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/DeleteTaskController.java similarity index 79% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/DeleteTaskController.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/DeleteTaskController.java index b34e6db..eef8b71 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/DeleteTaskController.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/DeleteTaskController.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.in.web; +package ch.unisg.roster.roster.adapter.in.web; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -6,9 +6,9 @@ import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import ch.unisg.assignment.assignment.application.port.in.DeleteTaskCommand; -import ch.unisg.assignment.assignment.application.port.in.DeleteTaskUseCase; -import ch.unisg.assignment.assignment.domain.Task; +import ch.unisg.roster.roster.application.port.in.DeleteTaskCommand; +import ch.unisg.roster.roster.application.port.in.DeleteTaskUseCase; +import ch.unisg.roster.roster.domain.Task; @RestController public class DeleteTaskController { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/NewTaskController.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java similarity index 81% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/NewTaskController.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java index 9faf2ec..af01346 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/NewTaskController.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/NewTaskController.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.in.web; +package ch.unisg.roster.roster.adapter.in.web; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -6,9 +6,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import ch.unisg.assignment.assignment.application.port.in.NewTaskCommand; -import ch.unisg.assignment.assignment.application.port.in.NewTaskUseCase; -import ch.unisg.assignment.assignment.domain.Task; +import ch.unisg.roster.roster.application.port.in.NewTaskCommand; +import ch.unisg.roster.roster.application.port.in.NewTaskUseCase; +import ch.unisg.roster.roster.domain.Task; @RestController public class NewTaskController { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/TaskCompletedController.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/TaskCompletedController.java similarity index 79% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/TaskCompletedController.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/TaskCompletedController.java index df89c7f..f81db32 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/TaskCompletedController.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/TaskCompletedController.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.in.web; +package ch.unisg.roster.roster.adapter.in.web; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -6,9 +6,9 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; -import ch.unisg.assignment.assignment.application.port.in.TaskCompletedCommand; -import ch.unisg.assignment.assignment.application.port.in.TaskCompletedUseCase; -import ch.unisg.assignment.assignment.domain.Task; +import ch.unisg.roster.roster.application.port.in.TaskCompletedCommand; +import ch.unisg.roster.roster.application.port.in.TaskCompletedUseCase; +import ch.unisg.roster.roster.domain.Task; @RestController public class TaskCompletedController { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/WebControllerExceptionHandler.java similarity index 93% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/WebControllerExceptionHandler.java index 19cce0d..f0a4974 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/in/web/WebControllerExceptionHandler.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/in/web/WebControllerExceptionHandler.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.in.web; +package ch.unisg.roster.roster.adapter.in.web; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java similarity index 89% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java index 1c02839..df444ca 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/GetAllExecutorInExecutorPoolByTypeAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.out.web; +package ch.unisg.roster.roster.adapter.out.web; import java.io.IOException; import java.net.URI; @@ -14,8 +14,8 @@ import org.springframework.context.annotation.Primary; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.out.GetAllExecutorInExecutorPoolByTypePort; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.application.port.out.GetAllExecutorInExecutorPoolByTypePort; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; @Component @Primary diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java similarity index 91% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java index 10638d3..6a6b7f7 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishNewTaskEventAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishNewTaskEventAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.out.web; +package ch.unisg.roster.roster.adapter.out.web; import java.io.IOException; import java.net.URI; @@ -12,8 +12,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.out.NewTaskEventPort; -import ch.unisg.assignment.assignment.domain.event.NewTaskEvent; +import ch.unisg.roster.roster.application.port.out.NewTaskEventPort; +import ch.unisg.roster.roster.domain.event.NewTaskEvent; @Component @Primary diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskAssignedEventAdapter.java similarity index 88% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskAssignedEventAdapter.java index 45a10f3..c71e306 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskAssignedEventAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskAssignedEventAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.out.web; +package ch.unisg.roster.roster.adapter.out.web; import java.io.IOException; import java.net.URI; @@ -13,8 +13,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.out.TaskAssignedEventPort; -import ch.unisg.assignment.assignment.domain.event.TaskAssignedEvent; +import ch.unisg.roster.roster.application.port.out.TaskAssignedEventPort; +import ch.unisg.roster.roster.domain.event.TaskAssignedEvent; @Component @Primary diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskCompletedEventAdapter.java similarity index 89% rename from assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java rename to roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskCompletedEventAdapter.java index e9c4944..7038291 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/adapter/out/web/PublishTaskCompletedEventAdapter.java +++ b/roster/src/main/java/ch/unisg/roster/roster/adapter/out/web/PublishTaskCompletedEventAdapter.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.adapter.out.web; +package ch.unisg.roster.roster.adapter.out.web; import java.io.IOException; import java.net.URI; @@ -13,8 +13,8 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.out.TaskCompletedEventPort; -import ch.unisg.assignment.assignment.domain.event.TaskCompletedEvent; +import ch.unisg.roster.roster.application.port.out.TaskCompletedEventPort; +import ch.unisg.roster.roster.domain.event.TaskCompletedEvent; @Component @Primary diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorAddedHandler.java b/roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorAddedHandler.java new file mode 100644 index 0000000..9545c07 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorAddedHandler.java @@ -0,0 +1,16 @@ +package ch.unisg.roster.roster.application.handler; + +import ch.unisg.roster.roster.application.port.in.ExecutorAddedEvent; +import ch.unisg.roster.roster.application.port.in.ExecutorAddedEventHandler; +import ch.unisg.roster.roster.domain.ExecutorRegistry; +import org.springframework.stereotype.Component; + +@Component +public class ExecutorAddedHandler implements ExecutorAddedEventHandler { + + @Override + public boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent) { + return ExecutorRegistry.getInstance().addExecutor(executorAddedEvent.getExecutorType(), + executorAddedEvent.getExecutorURI()); + } +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorRemovedHandler.java b/roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorRemovedHandler.java new file mode 100644 index 0000000..c6e3f68 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/application/handler/ExecutorRemovedHandler.java @@ -0,0 +1,19 @@ +package ch.unisg.roster.roster.application.handler; + +import ch.unisg.roster.roster.application.port.in.ExecutorRemovedEvent; +import ch.unisg.roster.roster.application.port.in.ExecutorRemovedEventHandler; +import ch.unisg.roster.roster.domain.ExecutorRegistry; +import org.springframework.stereotype.Component; + +/** + * Handler for executor removed events. It removes the executor from this roster's executor + * registry. + */ +@Component +public class ExecutorRemovedHandler implements ExecutorRemovedEventHandler { + + @Override + public boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent) { + return ExecutorRegistry.getInstance().removeExecutor(executorRemovedEvent.getExecutorURI()); + } +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskCommand.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ApplyForTaskCommand.java similarity index 82% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskCommand.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/in/ApplyForTaskCommand.java index bdc16d9..f03ef5f 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/ApplyForTaskCommand.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ApplyForTaskCommand.java @@ -1,8 +1,8 @@ -package ch.unisg.assignment.assignment.application.port.in; +package ch.unisg.roster.roster.application.port.in; import javax.validation.constraints.NotNull; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; import ch.unisg.common.validation.SelfValidating; import ch.unisg.common.valueobject.ExecutorURI; import lombok.EqualsAndHashCode; diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ApplyForTaskUseCase.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ApplyForTaskUseCase.java new file mode 100644 index 0000000..61b7bd4 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ApplyForTaskUseCase.java @@ -0,0 +1,7 @@ +package ch.unisg.roster.roster.application.port.in; + +import ch.unisg.roster.roster.domain.Task; + +public interface ApplyForTaskUseCase { + Task applyForTask(ApplyForTaskCommand applyForTaskCommand); +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskCommand.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/DeleteTaskCommand.java similarity index 80% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskCommand.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/in/DeleteTaskCommand.java index 7239acc..9f59dc3 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskCommand.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/DeleteTaskCommand.java @@ -1,8 +1,8 @@ -package ch.unisg.assignment.assignment.application.port.in; +package ch.unisg.roster.roster.application.port.in; import javax.validation.constraints.NotNull; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; import ch.unisg.common.validation.SelfValidating; import lombok.EqualsAndHashCode; import lombok.Value; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskUseCase.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/DeleteTaskUseCase.java similarity index 62% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskUseCase.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/in/DeleteTaskUseCase.java index e890e8b..2acfc63 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/DeleteTaskUseCase.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/DeleteTaskUseCase.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.application.port.in; +package ch.unisg.roster.roster.application.port.in; public interface DeleteTaskUseCase { boolean deleteTask(DeleteTaskCommand deleteTaskCommand); diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEvent.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEvent.java new file mode 100644 index 0000000..0e10b8e --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEvent.java @@ -0,0 +1,33 @@ +package ch.unisg.roster.roster.application.port.in; + +import lombok.Value; + +import javax.validation.constraints.NotNull; + +import ch.unisg.common.validation.SelfValidating; +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; + +/** + * Event that notifies the auction house that an executor has been added to this TAPAS application. + */ +@Value +public class ExecutorAddedEvent extends SelfValidating { + @NotNull + private final ExecutorURI executorURI; + + @NotNull + private final ExecutorType executorType; + + /** + * Constructs an executor added event. + * + * @param executorURI the identifier of the executor that was added to this TAPAS application + */ + public ExecutorAddedEvent(ExecutorURI executorURI, ExecutorType executorType) { + this.executorURI = executorURI; + this.executorType = executorType; + + this.validateSelf(); + } +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEventHandler.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEventHandler.java new file mode 100644 index 0000000..c7a9076 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorAddedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.roster.roster.application.port.in; + +public interface ExecutorAddedEventHandler { + + boolean handleNewExecutorEvent(ExecutorAddedEvent executorAddedEvent); +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEvent.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEvent.java new file mode 100644 index 0000000..8753683 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEvent.java @@ -0,0 +1,27 @@ +package ch.unisg.roster.roster.application.port.in; + +import lombok.Value; + +import javax.validation.constraints.NotNull; + +import ch.unisg.common.validation.SelfValidating; +import ch.unisg.common.valueobject.ExecutorURI; + +/** + * Event that notifies the auction house that an executor has been removed from this TAPAS application. + */ +@Value +public class ExecutorRemovedEvent extends SelfValidating { + @NotNull + private final ExecutorURI executorURI; + + /** + * Constructs an executor removed event. + * + * @param executorURI the identifier of the executor that was removed from this TAPAS application + */ + public ExecutorRemovedEvent(ExecutorURI executorURI) { + this.executorURI = executorURI; + this.validateSelf(); + } +} diff --git a/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEventHandler.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEventHandler.java new file mode 100644 index 0000000..79ee6a7 --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/ExecutorRemovedEventHandler.java @@ -0,0 +1,6 @@ +package ch.unisg.roster.roster.application.port.in; + +public interface ExecutorRemovedEventHandler { + + boolean handleExecutorRemovedEvent(ExecutorRemovedEvent executorRemovedEvent); +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskCommand.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskCommand.java similarity index 80% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskCommand.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskCommand.java index f06798b..92a7403 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskCommand.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskCommand.java @@ -1,8 +1,8 @@ -package ch.unisg.assignment.assignment.application.port.in; +package ch.unisg.roster.roster.application.port.in; import javax.validation.constraints.NotNull; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; import ch.unisg.common.validation.SelfValidating; import lombok.EqualsAndHashCode; import lombok.Value; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskUseCase.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskUseCase.java similarity index 62% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskUseCase.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskUseCase.java index 21f084e..f1bd733 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/NewTaskUseCase.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/NewTaskUseCase.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.application.port.in; +package ch.unisg.roster.roster.application.port.in; public interface NewTaskUseCase { boolean addNewTaskToQueue(NewTaskCommand newTaskCommand); diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedCommand.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/TaskCompletedCommand.java similarity index 91% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedCommand.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/in/TaskCompletedCommand.java index 08dc8eb..b7438c0 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedCommand.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/TaskCompletedCommand.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.application.port.in; +package ch.unisg.roster.roster.application.port.in; import javax.validation.constraints.NotNull; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedUseCase.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/TaskCompletedUseCase.java similarity index 64% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedUseCase.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/in/TaskCompletedUseCase.java index 1902952..51b305a 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/in/TaskCompletedUseCase.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/in/TaskCompletedUseCase.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.application.port.in; +package ch.unisg.roster.roster.application.port.in; public interface TaskCompletedUseCase { void taskCompleted(TaskCompletedCommand taskCompletedCommand); diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java similarity index 63% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java index 9f6c824..f32a3f5 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/GetAllExecutorInExecutorPoolByTypePort.java @@ -1,6 +1,6 @@ -package ch.unisg.assignment.assignment.application.port.out; +package ch.unisg.roster.roster.application.port.out; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; public interface GetAllExecutorInExecutorPoolByTypePort { /** diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/NewTaskEventPort.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/NewTaskEventPort.java similarity index 56% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/NewTaskEventPort.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/out/NewTaskEventPort.java index 243c7f2..75fda6d 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/NewTaskEventPort.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/NewTaskEventPort.java @@ -1,6 +1,6 @@ -package ch.unisg.assignment.assignment.application.port.out; +package ch.unisg.roster.roster.application.port.out; -import ch.unisg.assignment.assignment.domain.event.NewTaskEvent; +import ch.unisg.roster.roster.domain.event.NewTaskEvent; public interface NewTaskEventPort { /** diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskAssignedEventPort.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/TaskAssignedEventPort.java similarity index 60% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskAssignedEventPort.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/out/TaskAssignedEventPort.java index 5f55ec8..2bcb2ae 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskAssignedEventPort.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/TaskAssignedEventPort.java @@ -1,6 +1,6 @@ -package ch.unisg.assignment.assignment.application.port.out; +package ch.unisg.roster.roster.application.port.out; -import ch.unisg.assignment.assignment.domain.event.TaskAssignedEvent; +import ch.unisg.roster.roster.domain.event.TaskAssignedEvent; public interface TaskAssignedEventPort { /** diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskCompletedEventPort.java b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/TaskCompletedEventPort.java similarity index 58% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskCompletedEventPort.java rename to roster/src/main/java/ch/unisg/roster/roster/application/port/out/TaskCompletedEventPort.java index 83ad179..a8c11ef 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/port/out/TaskCompletedEventPort.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/port/out/TaskCompletedEventPort.java @@ -1,6 +1,6 @@ -package ch.unisg.assignment.assignment.application.port.out; +package ch.unisg.roster.roster.application.port.out; -import ch.unisg.assignment.assignment.domain.event.TaskCompletedEvent; +import ch.unisg.roster.roster.domain.event.TaskCompletedEvent; public interface TaskCompletedEventPort { /** diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java b/roster/src/main/java/ch/unisg/roster/roster/application/service/ApplyForTaskService.java similarity index 65% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java rename to roster/src/main/java/ch/unisg/roster/roster/application/service/ApplyForTaskService.java index dfb70e0..26b75aa 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/ApplyForTaskService.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/service/ApplyForTaskService.java @@ -1,15 +1,15 @@ -package ch.unisg.assignment.assignment.application.service; +package ch.unisg.roster.roster.application.service; import javax.transaction.Transactional; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.in.ApplyForTaskCommand; -import ch.unisg.assignment.assignment.application.port.in.ApplyForTaskUseCase; -import ch.unisg.assignment.assignment.application.port.out.TaskAssignedEventPort; -import ch.unisg.assignment.assignment.domain.Roster; -import ch.unisg.assignment.assignment.domain.Task; -import ch.unisg.assignment.assignment.domain.event.TaskAssignedEvent; +import ch.unisg.roster.roster.application.port.in.ApplyForTaskCommand; +import ch.unisg.roster.roster.application.port.in.ApplyForTaskUseCase; +import ch.unisg.roster.roster.application.port.out.TaskAssignedEventPort; +import ch.unisg.roster.roster.domain.Roster; +import ch.unisg.roster.roster.domain.Task; +import ch.unisg.roster.roster.domain.event.TaskAssignedEvent; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/DeleteTaskService.java b/roster/src/main/java/ch/unisg/roster/roster/application/service/DeleteTaskService.java similarity index 67% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/service/DeleteTaskService.java rename to roster/src/main/java/ch/unisg/roster/roster/application/service/DeleteTaskService.java index 7d67e4a..a6b4841 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/DeleteTaskService.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/service/DeleteTaskService.java @@ -1,12 +1,12 @@ -package ch.unisg.assignment.assignment.application.service; +package ch.unisg.roster.roster.application.service; import javax.transaction.Transactional; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.in.DeleteTaskCommand; -import ch.unisg.assignment.assignment.application.port.in.DeleteTaskUseCase; -import ch.unisg.assignment.assignment.domain.Roster; +import ch.unisg.roster.roster.application.port.in.DeleteTaskCommand; +import ch.unisg.roster.roster.application.port.in.DeleteTaskUseCase; +import ch.unisg.roster.roster.domain.Roster; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java b/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java similarity index 53% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java rename to roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java index d240a4b..588ed04 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/NewTaskService.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/service/NewTaskService.java @@ -1,16 +1,16 @@ -package ch.unisg.assignment.assignment.application.service; +package ch.unisg.roster.roster.application.service; import javax.transaction.Transactional; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.in.NewTaskCommand; -import ch.unisg.assignment.assignment.application.port.in.NewTaskUseCase; -import ch.unisg.assignment.assignment.application.port.out.GetAllExecutorInExecutorPoolByTypePort; -import ch.unisg.assignment.assignment.application.port.out.NewTaskEventPort; -import ch.unisg.assignment.assignment.domain.Roster; -import ch.unisg.assignment.assignment.domain.Task; -import ch.unisg.assignment.assignment.domain.event.NewTaskEvent; +import ch.unisg.roster.roster.application.port.in.NewTaskCommand; +import ch.unisg.roster.roster.application.port.in.NewTaskUseCase; +import ch.unisg.roster.roster.application.port.out.NewTaskEventPort; +import ch.unisg.roster.roster.domain.ExecutorRegistry; +import ch.unisg.roster.roster.domain.Roster; +import ch.unisg.roster.roster.domain.Task; +import ch.unisg.roster.roster.domain.event.NewTaskEvent; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @@ -19,7 +19,6 @@ import lombok.RequiredArgsConstructor; public class NewTaskService implements NewTaskUseCase { private final NewTaskEventPort newTaskEventPort; - private final GetAllExecutorInExecutorPoolByTypePort getAllExecutorInExecutorPoolByTypePort; /** * Checks if we can execute the give task, if yes the task gets added to the task queue and return true. @@ -29,9 +28,10 @@ public class NewTaskService implements NewTaskUseCase { @Override public boolean addNewTaskToQueue(NewTaskCommand command) { - // if (!getAllExecutorInExecutorPoolByTypePort.doesExecutorTypeExist(command.getTaskType())) { - // return false; - // } + ExecutorRegistry executorRegistry = ExecutorRegistry.getInstance(); + if (!executorRegistry.containsTaskType(command.getTaskType())) { + return false; + } Task task = new Task(command.getTaskID(), command.getTaskType()); diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/TaskCompletedService.java b/roster/src/main/java/ch/unisg/roster/roster/application/service/TaskCompletedService.java similarity index 63% rename from assignment/src/main/java/ch/unisg/assignment/assignment/application/service/TaskCompletedService.java rename to roster/src/main/java/ch/unisg/roster/roster/application/service/TaskCompletedService.java index 7c3e7f6..69b65d1 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/application/service/TaskCompletedService.java +++ b/roster/src/main/java/ch/unisg/roster/roster/application/service/TaskCompletedService.java @@ -1,14 +1,14 @@ -package ch.unisg.assignment.assignment.application.service; +package ch.unisg.roster.roster.application.service; import javax.transaction.Transactional; import org.springframework.stereotype.Component; -import ch.unisg.assignment.assignment.application.port.in.TaskCompletedCommand; -import ch.unisg.assignment.assignment.application.port.in.TaskCompletedUseCase; -import ch.unisg.assignment.assignment.application.port.out.TaskCompletedEventPort; -import ch.unisg.assignment.assignment.domain.Roster; -import ch.unisg.assignment.assignment.domain.event.TaskCompletedEvent; +import ch.unisg.roster.roster.application.port.in.TaskCompletedCommand; +import ch.unisg.roster.roster.application.port.in.TaskCompletedUseCase; +import ch.unisg.roster.roster.application.port.out.TaskCompletedEventPort; +import ch.unisg.roster.roster.domain.Roster; +import ch.unisg.roster.roster.domain.event.TaskCompletedEvent; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/ExecutorInfo.java b/roster/src/main/java/ch/unisg/roster/roster/domain/ExecutorInfo.java similarity index 67% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/ExecutorInfo.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/ExecutorInfo.java index 58b47dc..eb32ec0 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/ExecutorInfo.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/ExecutorInfo.java @@ -1,6 +1,6 @@ -package ch.unisg.assignment.assignment.domain; +package ch.unisg.roster.roster.domain; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; import ch.unisg.common.valueobject.ExecutorURI; import lombok.Getter; import lombok.Setter; diff --git a/roster/src/main/java/ch/unisg/roster/roster/domain/ExecutorRegistry.java b/roster/src/main/java/ch/unisg/roster/roster/domain/ExecutorRegistry.java new file mode 100644 index 0000000..4ddba0f --- /dev/null +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/ExecutorRegistry.java @@ -0,0 +1,92 @@ +package ch.unisg.roster.roster.domain; + +import java.util.*; + +import ch.unisg.common.valueobject.ExecutorURI; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; + +/** + * 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 class is a singleton. + */ +public class ExecutorRegistry { + private static ExecutorRegistry registry; + + private final Map> executors; + + private ExecutorRegistry() { + this.executors = new Hashtable<>(); + } + + public static synchronized ExecutorRegistry getInstance() { + if (registry == null) { + registry = new ExecutorRegistry(); + } + + return registry; + } + + /** + * Adds an executor to the registry for a given task type. + * + * @param taskType the type of the task + * @param executorIdentifier the identifier of the executor (can be any string) + * @return true unless a runtime exception occurs + */ + public boolean addExecutor(ExecutorType executorType, ExecutorURI executorURI) { + Set taskTypeExecs = executors.getOrDefault(executorType, + Collections.synchronizedSet(new HashSet<>())); + + taskTypeExecs.add(executorURI); + executors.put(executorType, taskTypeExecs); + + return true; + } + + /** + * Removes an executor from the registry. The executor is disassociated from all known task types. + * + * @param executorURI the identifier of the executor + * @return true unless a runtime exception occurs + */ + public boolean removeExecutor(ExecutorURI executorURI) { + Iterator iterator = executors.keySet().iterator(); + + while (iterator.hasNext()) { + ExecutorType executorType = iterator.next(); + Set set = executors.get(executorType); + + set.remove(executorURI); + + if (set.isEmpty()) { + iterator.remove(); + } + } + + return true; + } + + /** + * Checks if the registry contains an executor for a given task type. Used during task creation + * to decide if a task can be executed. + * + * @param taskType the task type being auctioned + * @return + */ + public boolean containsTaskType(ExecutorType taskType) { + return executors.containsKey(taskType); + } + + /** + * Adds a list of executors to current executor list. Should only be used on startup to + * add all available executors from the executor pool to the registry. + * + * @param executors the initial executors + * @return + */ + public void init(Map> executors) { + this.executors.putAll(executors); + } + +} diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java b/roster/src/main/java/ch/unisg/roster/roster/domain/Roster.java similarity index 95% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/Roster.java index fb259c1..cc9a0a6 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Roster.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/Roster.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.domain; +package ch.unisg.roster.roster.domain; import java.util.ArrayList; import java.util.Arrays; @@ -6,7 +6,7 @@ import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; import ch.unisg.common.valueobject.ExecutorURI; public class Roster { diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/RosterItem.java b/roster/src/main/java/ch/unisg/roster/roster/domain/RosterItem.java similarity index 90% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/RosterItem.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/RosterItem.java index b405f44..cc39c6c 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/RosterItem.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/RosterItem.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.domain; +package ch.unisg.roster.roster.domain; import ch.unisg.common.valueobject.ExecutorURI; import lombok.Getter; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Task.java b/roster/src/main/java/ch/unisg/roster/roster/domain/Task.java similarity index 82% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/Task.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/Task.java index 7daa738..40ef9fa 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/Task.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/Task.java @@ -1,6 +1,6 @@ -package ch.unisg.assignment.assignment.domain; +package ch.unisg.roster.roster.domain; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; import lombok.Getter; import lombok.Setter; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/NewTaskEvent.java b/roster/src/main/java/ch/unisg/roster/roster/domain/event/NewTaskEvent.java similarity index 56% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/NewTaskEvent.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/event/NewTaskEvent.java index 34e7f0b..1457f1d 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/NewTaskEvent.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/event/NewTaskEvent.java @@ -1,6 +1,6 @@ -package ch.unisg.assignment.assignment.domain.event; +package ch.unisg.roster.roster.domain.event; -import ch.unisg.assignment.assignment.domain.valueobject.ExecutorType; +import ch.unisg.roster.roster.domain.valueobject.ExecutorType; public class NewTaskEvent { public final ExecutorType taskType; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/TaskAssignedEvent.java b/roster/src/main/java/ch/unisg/roster/roster/domain/event/TaskAssignedEvent.java similarity index 74% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/TaskAssignedEvent.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/event/TaskAssignedEvent.java index d0178d4..9c57270 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/TaskAssignedEvent.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/event/TaskAssignedEvent.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.domain.event; +package ch.unisg.roster.roster.domain.event; public class TaskAssignedEvent { public final String taskID; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/TaskCompletedEvent.java b/roster/src/main/java/ch/unisg/roster/roster/domain/event/TaskCompletedEvent.java similarity index 85% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/TaskCompletedEvent.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/event/TaskCompletedEvent.java index 432a8f0..926f601 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/event/TaskCompletedEvent.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/event/TaskCompletedEvent.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.domain.event; +package ch.unisg.roster.roster.domain.event; public class TaskCompletedEvent { public final String taskID; diff --git a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/ExecutorType.java b/roster/src/main/java/ch/unisg/roster/roster/domain/valueobject/ExecutorType.java similarity index 74% rename from assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/ExecutorType.java rename to roster/src/main/java/ch/unisg/roster/roster/domain/valueobject/ExecutorType.java index bc5f467..72368e3 100644 --- a/assignment/src/main/java/ch/unisg/assignment/assignment/domain/valueobject/ExecutorType.java +++ b/roster/src/main/java/ch/unisg/roster/roster/domain/valueobject/ExecutorType.java @@ -1,4 +1,4 @@ -package ch.unisg.assignment.assignment.domain.valueobject; +package ch.unisg.roster.roster.domain.valueobject; import lombok.Value; diff --git a/assignment/src/main/resources/application.properties b/roster/src/main/resources/application.properties similarity index 100% rename from assignment/src/main/resources/application.properties rename to roster/src/main/resources/application.properties diff --git a/assignment/src/test/java/ch/unisg/assignment/AssignmentApplicationTests.java b/roster/src/test/java/ch/unisg/roster/RosterApplicationTests.java similarity index 70% rename from assignment/src/test/java/ch/unisg/assignment/AssignmentApplicationTests.java rename to roster/src/test/java/ch/unisg/roster/RosterApplicationTests.java index 9da24b5..5ee712b 100644 --- a/assignment/src/test/java/ch/unisg/assignment/AssignmentApplicationTests.java +++ b/roster/src/test/java/ch/unisg/roster/RosterApplicationTests.java @@ -1,10 +1,10 @@ -package ch.unisg.assignment; +package ch.unisg.roster; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest -class AssignmentApplicationTests { +class RosterApplicationTests { @Test void contextLoads() { From ee06c0b80e0b325eedfe951426a0a55e6fc32fca Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 8 Nov 2021 14:48:12 +0100 Subject: [PATCH 16/40] error fixes --- .../ExecutorcomputationApplication.java | 0 .../executor/adapter/in/web/TaskAvailableController.java | 0 .../executor/application/service/TaskAvailableService.java | 0 .../executor/domain/Executor.java | 0 .../ExecutorcomputationApplicationTests.java | 0 .../ExecutorrobotApplication.java | 0 .../executor/adapter/in/web/TaskAvailableController.java | 0 .../executor/adapter/out/DeleteUserFromRobotAdapter.java | 0 .../executor/adapter/out/InstructionToRobotAdapter.java | 0 .../executor/adapter/out/UserToRobotAdapter.java | 0 .../application/port/out/DeleteUserFromRobotPort.java | 0 .../executor/application/port/out/InstructionToRobotPort.java | 0 .../executor/application/port/out/UserToRobotPort.java | 0 .../executor/application/service/TaskAvailableService.java | 0 .../executor/domain/Executor.java | 4 +--- .../ExecutorrobotApplicationTests.java | 0 16 files changed, 1 insertion(+), 3 deletions(-) rename executorcomputation/src/main/java/ch/unisg/{executor2 => executorcomputation}/ExecutorcomputationApplication.java (100%) rename executorcomputation/src/main/java/ch/unisg/{executor2 => executorcomputation}/executor/adapter/in/web/TaskAvailableController.java (100%) rename executorcomputation/src/main/java/ch/unisg/{executor2 => executorcomputation}/executor/application/service/TaskAvailableService.java (100%) rename executorcomputation/src/main/java/ch/unisg/{executor2 => executorcomputation}/executor/domain/Executor.java (100%) rename executorcomputation/src/test/java/ch/unisg/{executor2 => executorcomputation}/ExecutorcomputationApplicationTests.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/ExecutorrobotApplication.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/adapter/in/web/TaskAvailableController.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/adapter/out/DeleteUserFromRobotAdapter.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/adapter/out/InstructionToRobotAdapter.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/adapter/out/UserToRobotAdapter.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/application/port/out/DeleteUserFromRobotPort.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/application/port/out/InstructionToRobotPort.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/application/port/out/UserToRobotPort.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/application/service/TaskAvailableService.java (100%) rename executorrobot/src/main/java/ch/unisg/{executor1 => executorrobot}/executor/domain/Executor.java (95%) rename executorrobot/src/test/java/ch/unisg/{executor1 => executorrobot}/ExecutorrobotApplicationTests.java (100%) diff --git a/executorcomputation/src/main/java/ch/unisg/executor2/ExecutorcomputationApplication.java b/executorcomputation/src/main/java/ch/unisg/executorcomputation/ExecutorcomputationApplication.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executor2/ExecutorcomputationApplication.java rename to executorcomputation/src/main/java/ch/unisg/executorcomputation/ExecutorcomputationApplication.java diff --git a/executorcomputation/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java b/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/adapter/in/web/TaskAvailableController.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executor2/executor/adapter/in/web/TaskAvailableController.java rename to executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/adapter/in/web/TaskAvailableController.java diff --git a/executorcomputation/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java b/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/application/service/TaskAvailableService.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executor2/executor/application/service/TaskAvailableService.java rename to executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/application/service/TaskAvailableService.java diff --git a/executorcomputation/src/main/java/ch/unisg/executor2/executor/domain/Executor.java b/executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java similarity index 100% rename from executorcomputation/src/main/java/ch/unisg/executor2/executor/domain/Executor.java rename to executorcomputation/src/main/java/ch/unisg/executorcomputation/executor/domain/Executor.java diff --git a/executorcomputation/src/test/java/ch/unisg/executor2/ExecutorcomputationApplicationTests.java b/executorcomputation/src/test/java/ch/unisg/executorcomputation/ExecutorcomputationApplicationTests.java similarity index 100% rename from executorcomputation/src/test/java/ch/unisg/executor2/ExecutorcomputationApplicationTests.java rename to executorcomputation/src/test/java/ch/unisg/executorcomputation/ExecutorcomputationApplicationTests.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/ExecutorrobotApplication.java b/executorrobot/src/main/java/ch/unisg/executorrobot/ExecutorrobotApplication.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/ExecutorrobotApplication.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/ExecutorrobotApplication.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/in/web/TaskAvailableController.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/in/web/TaskAvailableController.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/in/web/TaskAvailableController.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/DeleteUserFromRobotAdapter.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/DeleteUserFromRobotAdapter.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/DeleteUserFromRobotAdapter.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/InstructionToRobotAdapter.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/InstructionToRobotAdapter.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/InstructionToRobotAdapter.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/UserToRobotAdapter.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/adapter/out/UserToRobotAdapter.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/adapter/out/UserToRobotAdapter.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/DeleteUserFromRobotPort.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/DeleteUserFromRobotPort.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/DeleteUserFromRobotPort.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/InstructionToRobotPort.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/InstructionToRobotPort.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/InstructionToRobotPort.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/UserToRobotPort.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/application/port/out/UserToRobotPort.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/port/out/UserToRobotPort.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/service/TaskAvailableService.java similarity index 100% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/application/service/TaskAvailableService.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/application/service/TaskAvailableService.java diff --git a/executorrobot/src/main/java/ch/unisg/executor1/executor/domain/Executor.java b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java similarity index 95% rename from executorrobot/src/main/java/ch/unisg/executor1/executor/domain/Executor.java rename to executorrobot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java index 44ddcb4..4124e9e 100644 --- a/executorrobot/src/main/java/ch/unisg/executor1/executor/domain/Executor.java +++ b/executorrobot/src/main/java/ch/unisg/executorrobot/executor/domain/Executor.java @@ -1,7 +1,5 @@ package ch.unisg.executorrobot.executor.domain; -import java.net.http.HttpClient; -import java.net.http.HttpResponse; import java.util.concurrent.TimeUnit; import ch.unisg.executorrobot.executor.adapter.out.DeleteUserFromRobotAdapter; @@ -30,7 +28,7 @@ public class Executor extends ExecutorBase { @Override protected - String execution() { + String execution(String... input) { String key = userToRobotPort.userToRobot(); try { diff --git a/executorrobot/src/test/java/ch/unisg/executor1/ExecutorrobotApplicationTests.java b/executorrobot/src/test/java/ch/unisg/executorrobot/ExecutorrobotApplicationTests.java similarity index 100% rename from executorrobot/src/test/java/ch/unisg/executor1/ExecutorrobotApplicationTests.java rename to executorrobot/src/test/java/ch/unisg/executorrobot/ExecutorrobotApplicationTests.java From f2fb9450640cafe01af6c1bd6f34cb7f57a4f6b7 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 8 Nov 2021 15:16:26 +0100 Subject: [PATCH 17/40] naming error fixes --- executor-base/pom.xml | 4 ++-- executorcomputation/pom.xml | 4 ++-- executorrobot/pom.xml | 4 ++-- roster/pom.xml | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/executor-base/pom.xml b/executor-base/pom.xml index 80c9739..7893d45 100644 --- a/executor-base/pom.xml +++ b/executor-base/pom.xml @@ -9,9 +9,9 @@ ch.unisg - executorBase + executorbase 0.0.1-SNAPSHOT - executorBase + executorbase Demo project for Spring Boot 11 diff --git a/executorcomputation/pom.xml b/executorcomputation/pom.xml index f422c55..b9e45ac 100644 --- a/executorcomputation/pom.xml +++ b/executorcomputation/pom.xml @@ -11,7 +11,7 @@ ch.unisg executorcomputation 0.0.1-SNAPSHOT - executor2 + executorcomputation Demo project for Spring Boot 11 @@ -42,7 +42,7 @@ ch.unisg - executorBase + executorbase 0.0.1-SNAPSHOT compile diff --git a/executorrobot/pom.xml b/executorrobot/pom.xml index f5348d4..101c268 100644 --- a/executorrobot/pom.xml +++ b/executorrobot/pom.xml @@ -11,7 +11,7 @@ ch.unisg executorrobot 0.0.1-SNAPSHOT - executor1 + executorrobot Demo project for Spring Boot 11 @@ -42,7 +42,7 @@ ch.unisg - executorBase + executorbase 0.0.1-SNAPSHOT diff --git a/roster/pom.xml b/roster/pom.xml index f51bff7..791e0d0 100644 --- a/roster/pom.xml +++ b/roster/pom.xml @@ -9,9 +9,9 @@ ch.unisg - assignment + roster 0.0.1-SNAPSHOT - assignment + roster Demo project for Spring Boot 11 From 8cfdd5ff094fd8e31c569a46e862928e8aea7d8a Mon Sep 17 00:00:00 2001 From: reynisson Date: Tue, 9 Nov 2021 21:25:02 +0100 Subject: [PATCH 18/40] Fixed some spelling mistakes and fixed an error in the Task list that produced a build error --- ...torsInExecutorPoolByTypeWebController.java} | 18 +++++++++--------- ...AllExecutorInExecutorPoolByTypeUseCase.java | 9 --------- ...AllExecutorsInExecutorPoolByTypeQuery.java} | 4 ++-- ...llExecutorsInExecutorPoolByTypeUseCase.java | 9 +++++++++ ...lExecutorsInExecutorPoolByTypeService.java} | 8 ++++---- .../application/service/DeleteTaskService.java | 8 ++++---- 6 files changed, 28 insertions(+), 28 deletions(-) rename executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/{GetAllExecutorInExecutorPoolByTypeWebController.java => GetAllExecutorsInExecutorPoolByTypeWebController.java} (50%) delete mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorInExecutorPoolByTypeUseCase.java rename executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/{GetAllExecutorInExecutorPoolByTypeQuery.java => GetAllExecutorsInExecutorPoolByTypeQuery.java} (64%) create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorsInExecutorPoolByTypeUseCase.java rename executor-pool/src/main/java/ch/unisg/executorpool/application/service/{GetAllExecutorInExecutorPoolByTypeService.java => GetAllExecutorsInExecutorPoolByTypeService.java} (56%) diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorInExecutorPoolByTypeWebController.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolByTypeWebController.java similarity index 50% rename from executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorInExecutorPoolByTypeWebController.java rename to executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolByTypeWebController.java index 2595781..dbea300 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorInExecutorPoolByTypeWebController.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolByTypeWebController.java @@ -1,7 +1,7 @@ package ch.unisg.executorpool.adapter.in.web; -import ch.unisg.executorpool.application.port.in.GetAllExecutorInExecutorPoolByTypeQuery; -import ch.unisg.executorpool.application.port.in.GetAllExecutorInExecutorPoolByTypeUseCase; +import ch.unisg.executorpool.application.port.in.GetAllExecutorsInExecutorPoolByTypeQuery; +import ch.unisg.executorpool.application.port.in.GetAllExecutorsInExecutorPoolByTypeUseCase; import ch.unisg.executorpool.domain.ExecutorClass; import ch.unisg.executorpool.domain.ExecutorClass.ExecutorTaskType; import org.springframework.http.HttpHeaders; @@ -14,17 +14,17 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController -public class GetAllExecutorInExecutorPoolByTypeWebController { - private final GetAllExecutorInExecutorPoolByTypeUseCase getAllExecutorInExecutorPoolByTypeUseCase; +public class GetAllExecutorsInExecutorPoolByTypeWebController { + private final GetAllExecutorsInExecutorPoolByTypeUseCase getAllExecutorsInExecutorPoolByTypeUseCase; - public GetAllExecutorInExecutorPoolByTypeWebController(GetAllExecutorInExecutorPoolByTypeUseCase getAllExecutorInExecutorPoolByTypeUseCase){ - this.getAllExecutorInExecutorPoolByTypeUseCase = getAllExecutorInExecutorPoolByTypeUseCase; + public GetAllExecutorsInExecutorPoolByTypeWebController(GetAllExecutorsInExecutorPoolByTypeUseCase getAllExecutorInExecutorPoolByTypeUseCase){ + this.getAllExecutorsInExecutorPoolByTypeUseCase = getAllExecutorInExecutorPoolByTypeUseCase; } - @GetMapping(path = "/executor-pool/GetAllExecutorInExecutorPoolByType/{taskType}") + @GetMapping(path = "/executor-pool/GetAllExecutorsInExecutorPoolByType/{taskType}") public ResponseEntity getAllExecutorInExecutorPoolByType(@PathVariable("taskType") String taskType){ - GetAllExecutorInExecutorPoolByTypeQuery query = new GetAllExecutorInExecutorPoolByTypeQuery(new ExecutorTaskType(taskType)); - List matchedExecutors = getAllExecutorInExecutorPoolByTypeUseCase.getAllExecutorInExecutorPoolByType(query); + GetAllExecutorsInExecutorPoolByTypeQuery query = new GetAllExecutorsInExecutorPoolByTypeQuery(new ExecutorTaskType(taskType)); + List matchedExecutors = getAllExecutorsInExecutorPoolByTypeUseCase.getAllExecutorsInExecutorPoolByType(query); // Add the content type as a response header HttpHeaders responseHeaders = new HttpHeaders(); diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorInExecutorPoolByTypeUseCase.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorInExecutorPoolByTypeUseCase.java deleted file mode 100644 index 9f612bf..0000000 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorInExecutorPoolByTypeUseCase.java +++ /dev/null @@ -1,9 +0,0 @@ -package ch.unisg.executorpool.application.port.in; - -import ch.unisg.executorpool.domain.ExecutorClass; - -import java.util.List; - -public interface GetAllExecutorInExecutorPoolByTypeUseCase { - List getAllExecutorInExecutorPoolByType(GetAllExecutorInExecutorPoolByTypeQuery query); -} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorInExecutorPoolByTypeQuery.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorsInExecutorPoolByTypeQuery.java similarity index 64% rename from executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorInExecutorPoolByTypeQuery.java rename to executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorsInExecutorPoolByTypeQuery.java index c812eab..079e7e1 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorInExecutorPoolByTypeQuery.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorsInExecutorPoolByTypeQuery.java @@ -7,11 +7,11 @@ import lombok.Value; import javax.validation.constraints.NotNull; @Value -public class GetAllExecutorInExecutorPoolByTypeQuery extends SelfValidating { +public class GetAllExecutorsInExecutorPoolByTypeQuery extends SelfValidating { @NotNull private final ExecutorTaskType executorTaskType; - public GetAllExecutorInExecutorPoolByTypeQuery(ExecutorTaskType executorTaskType){ + public GetAllExecutorsInExecutorPoolByTypeQuery(ExecutorTaskType executorTaskType){ this.executorTaskType = executorTaskType; this.validateSelf(); } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorsInExecutorPoolByTypeUseCase.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorsInExecutorPoolByTypeUseCase.java new file mode 100644 index 0000000..4821284 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/GetAllExecutorsInExecutorPoolByTypeUseCase.java @@ -0,0 +1,9 @@ +package ch.unisg.executorpool.application.port.in; + +import ch.unisg.executorpool.domain.ExecutorClass; + +import java.util.List; + +public interface GetAllExecutorsInExecutorPoolByTypeUseCase { + List getAllExecutorsInExecutorPoolByType(GetAllExecutorsInExecutorPoolByTypeQuery query); +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/GetAllExecutorInExecutorPoolByTypeService.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/GetAllExecutorsInExecutorPoolByTypeService.java similarity index 56% rename from executor-pool/src/main/java/ch/unisg/executorpool/application/service/GetAllExecutorInExecutorPoolByTypeService.java rename to executor-pool/src/main/java/ch/unisg/executorpool/application/service/GetAllExecutorsInExecutorPoolByTypeService.java index 74988b2..00d1636 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/GetAllExecutorInExecutorPoolByTypeService.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/GetAllExecutorsInExecutorPoolByTypeService.java @@ -1,7 +1,7 @@ package ch.unisg.executorpool.application.service; -import ch.unisg.executorpool.application.port.in.GetAllExecutorInExecutorPoolByTypeQuery; -import ch.unisg.executorpool.application.port.in.GetAllExecutorInExecutorPoolByTypeUseCase; +import ch.unisg.executorpool.application.port.in.GetAllExecutorsInExecutorPoolByTypeQuery; +import ch.unisg.executorpool.application.port.in.GetAllExecutorsInExecutorPoolByTypeUseCase; import ch.unisg.executorpool.domain.ExecutorClass; import ch.unisg.executorpool.domain.ExecutorPool; import lombok.RequiredArgsConstructor; @@ -13,10 +13,10 @@ import java.util.List; @RequiredArgsConstructor @Component @Transactional -public class GetAllExecutorInExecutorPoolByTypeService implements GetAllExecutorInExecutorPoolByTypeUseCase { +public class GetAllExecutorsInExecutorPoolByTypeService implements GetAllExecutorsInExecutorPoolByTypeUseCase { @Override - public List getAllExecutorInExecutorPoolByType(GetAllExecutorInExecutorPoolByTypeQuery query){ + public List getAllExecutorsInExecutorPoolByType(GetAllExecutorsInExecutorPoolByTypeQuery query){ ExecutorPool executorPool = ExecutorPool.getExecutorPool(); return executorPool.getAllExecutorsByType(query.getExecutorTaskType()); } diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java index f865f4c..35685a3 100644 --- a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/DeleteTaskService.java @@ -5,6 +5,7 @@ import ch.unisg.tapastasks.tasks.application.port.in.DeleteTaskCommand; import ch.unisg.tapastasks.tasks.application.port.in.DeleteTaskUseCase; import ch.unisg.tapastasks.tasks.domain.Task; import ch.unisg.tapastasks.tasks.domain.TaskList; +import jdk.jshell.spi.ExecutionControl; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @@ -23,11 +24,10 @@ public class DeleteTaskService implements DeleteTaskUseCase { Optional updatedTask = taskList.retrieveTaskById(command.getTaskId()); Task newTask = updatedTask.get(); // TODO: Fill in the right condition into the if-statement and the else-statement - if (/*the task can be deleted*/){ + if (true){ return taskList.deleteTaskById(command.getTaskId()); - } else { - /*send message back to TaskList that the task cannot be deleted*/ } - + // TODO Handle with a return message + return Optional.empty(); } } From ec8ff4b3bac9fba1de12e72698acf8d62b5a0354 Mon Sep 17 00:00:00 2001 From: reynisson Date: Tue, 9 Nov 2021 22:56:11 +0100 Subject: [PATCH 19/40] Changed ExecutorIp and ExecutorPort to ExecutorUri. Also made all necessary changes for it to work --- .../formats/ExecutorJsonRepresentation.java | 51 +++++++++++++++++++ ...ewExecutorToExecutorPoolWebController.java | 13 +++-- .../adapter/in/web/ExecutorMediaType.java | 38 -------------- ...torsInExecutorPoolByTypeWebController.java | 5 +- ...lExecutorsInExecutorPoolWebController.java | 5 +- ...ExecutorFromExecutorPoolWebController.java | 14 +++-- .../AddNewExecutorToExecutorPoolCommand.java | 13 ++--- ...RemoveExecutorFromExecutorPoolCommand.java | 14 ++--- .../AddNewExecutorToExecutorPoolService.java | 2 +- ...RemoveExecutorFromExecutorPoolService.java | 2 +- .../executorpool/domain/ExecutorClass.java | 27 ++++------ .../executorpool/domain/ExecutorPool.java | 18 +++---- 12 files changed, 103 insertions(+), 99 deletions(-) create mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/formats/ExecutorJsonRepresentation.java delete mode 100644 executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/ExecutorMediaType.java diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/formats/ExecutorJsonRepresentation.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/formats/ExecutorJsonRepresentation.java new file mode 100644 index 0000000..3c8f6e4 --- /dev/null +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/common/formats/ExecutorJsonRepresentation.java @@ -0,0 +1,51 @@ +package ch.unisg.executorpool.adapter.common.formats; + +import ch.unisg.executorpool.domain.ExecutorClass; +import lombok.Getter; +import lombok.Setter; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.List; + +public class ExecutorJsonRepresentation { + public static final String EXECUTOR_MEDIA_TYPE = "application/json"; + + @Getter @Setter + private String executorUri; + + @Getter @Setter + private String executorTaskType; + + // TODO Check if this need Setters. Also applies to AuctionJsonRepresentation + public ExecutorJsonRepresentation(String executorUri, String executorTaskType){ + this.executorUri = executorUri; + this.executorTaskType = executorTaskType; + } + + public static String serialize(ExecutorClass executorClass) { + JSONObject payload = new JSONObject(); + + payload.put("executorUri", executorClass.getExecutorUri().getValue()); + payload.put("executorTaskType", executorClass.getExecutorTaskType().getValue()); + + return payload.toString(); + } + + public static String serialize(List listOfExecutors) { + JSONArray jsonArray = new JSONArray(); + + for (ExecutorClass executor: listOfExecutors) { + JSONObject jsonObject = new JSONObject(); + + jsonObject.put("executorUri", executor.getExecutorUri().getValue()); + jsonObject.put("executorTaskType", executor.getExecutorTaskType().getValue()); + + jsonArray.put(jsonObject); + } + + return jsonArray.toString(); + } + + private ExecutorJsonRepresentation() { } +} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java index 7967b6b..5a2dc09 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/AddNewExecutorToExecutorPoolWebController.java @@ -1,5 +1,6 @@ package ch.unisg.executorpool.adapter.in.web; +import ch.unisg.executorpool.adapter.common.formats.ExecutorJsonRepresentation; import ch.unisg.executorpool.application.port.in.AddNewExecutorToExecutorPoolUseCase; import ch.unisg.executorpool.application.port.in.AddNewExecutorToExecutorPoolCommand; import ch.unisg.executorpool.domain.ExecutorClass; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import javax.validation.ConstraintViolationException; +import java.net.URI; @RestController public class AddNewExecutorToExecutorPoolWebController { @@ -20,19 +22,20 @@ public class AddNewExecutorToExecutorPoolWebController { this.addNewExecutorToExecutorPoolUseCase = addNewExecutorToExecutorPoolUseCase; } - @PostMapping(path = "/executor-pool/AddExecutor", consumes = {ExecutorMediaType.EXECUTOR_MEDIA_TYPE}) - public ResponseEntity addNewExecutorToExecutorPool(@RequestBody ExecutorClass executorClass){ + @PostMapping(path = "/executor-pool/AddExecutor", consumes = {ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE}) + public ResponseEntity addNewExecutorToExecutorPool(@RequestBody ExecutorJsonRepresentation payload){ try{ AddNewExecutorToExecutorPoolCommand command = new AddNewExecutorToExecutorPoolCommand( - executorClass.getExecutorIp(), executorClass.getExecutorPort(), executorClass.getExecutorTaskType() + new ExecutorClass.ExecutorUri(URI.create(payload.getExecutorUri())), + new ExecutorClass.ExecutorTaskType(payload.getExecutorTaskType()) ); ExecutorClass newExecutor = addNewExecutorToExecutorPoolUseCase.addNewExecutorToExecutorPool(command); HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorMediaType.EXECUTOR_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE); - return new ResponseEntity<>(ExecutorMediaType.serialize(newExecutor), responseHeaders, HttpStatus.CREATED); + return new ResponseEntity<>(ExecutorJsonRepresentation.serialize(newExecutor), responseHeaders, HttpStatus.CREATED); } catch (ConstraintViolationException e){ throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/ExecutorMediaType.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/ExecutorMediaType.java deleted file mode 100644 index 0ca4e1f..0000000 --- a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/ExecutorMediaType.java +++ /dev/null @@ -1,38 +0,0 @@ -package ch.unisg.executorpool.adapter.in.web; - -import ch.unisg.executorpool.domain.ExecutorClass; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.util.List; - -final public class ExecutorMediaType { - public static final String EXECUTOR_MEDIA_TYPE = "application/json"; - - public static String serialize(ExecutorClass executorClass) { - JSONObject payload = new JSONObject(); - - payload.put("executorIp", executorClass.getExecutorIp().getValue()); - payload.put("executorPort", executorClass.getExecutorPort().getValue()); - payload.put("executorTaskType", executorClass.getExecutorTaskType().getValue()); - - return payload.toString(); - } - - public static String serialize(List listOfExecutors) { - String serializedList = "[ \n"; - - for (ExecutorClass executor: listOfExecutors) { - serializedList += serialize(executor) + "\n"; - } - - // return serializedList + "\n ]"; - JSONArray jsonArray = new JSONArray(); - JSONObject jsonObject = new JSONObject(); - jsonObject.put("executorIp", "localhost"); - jsonArray.put(jsonObject); - return jsonArray.toString(); - } - - private ExecutorMediaType() { } -} diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolByTypeWebController.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolByTypeWebController.java index dbea300..8c7ce3d 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolByTypeWebController.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolByTypeWebController.java @@ -1,5 +1,6 @@ package ch.unisg.executorpool.adapter.in.web; +import ch.unisg.executorpool.adapter.common.formats.ExecutorJsonRepresentation; import ch.unisg.executorpool.application.port.in.GetAllExecutorsInExecutorPoolByTypeQuery; import ch.unisg.executorpool.application.port.in.GetAllExecutorsInExecutorPoolByTypeUseCase; import ch.unisg.executorpool.domain.ExecutorClass; @@ -28,8 +29,8 @@ public class GetAllExecutorsInExecutorPoolByTypeWebController { // Add the content type as a response header HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorMediaType.EXECUTOR_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE); - return new ResponseEntity<>(ExecutorMediaType.serialize(matchedExecutors), responseHeaders, HttpStatus.OK); + return new ResponseEntity<>(ExecutorJsonRepresentation.serialize(matchedExecutors), responseHeaders, HttpStatus.OK); } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolWebController.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolWebController.java index 70a5fd2..13a631a 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolWebController.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/GetAllExecutorsInExecutorPoolWebController.java @@ -1,5 +1,6 @@ package ch.unisg.executorpool.adapter.in.web; +import ch.unisg.executorpool.adapter.common.formats.ExecutorJsonRepresentation; import ch.unisg.executorpool.application.port.in.GetAllExecutorsInExecutorPoolUseCase; import ch.unisg.executorpool.domain.ExecutorClass; import org.springframework.http.HttpHeaders; @@ -24,8 +25,8 @@ public class GetAllExecutorsInExecutorPoolWebController { // Add the content type as a response header HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorMediaType.EXECUTOR_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE); - return new ResponseEntity<>(ExecutorMediaType.serialize(executorClassList), responseHeaders, HttpStatus.OK); + return new ResponseEntity<>(ExecutorJsonRepresentation.serialize(executorClassList), responseHeaders, HttpStatus.OK); } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/RemoveExecutorFromExecutorPoolWebController.java b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/RemoveExecutorFromExecutorPoolWebController.java index 69bbde3..28c3511 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/RemoveExecutorFromExecutorPoolWebController.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/adapter/in/web/RemoveExecutorFromExecutorPoolWebController.java @@ -1,5 +1,6 @@ package ch.unisg.executorpool.adapter.in.web; +import ch.unisg.executorpool.adapter.common.formats.ExecutorJsonRepresentation; import ch.unisg.executorpool.application.port.in.RemoveExecutorFromExecutorPoolCommand; import ch.unisg.executorpool.application.port.in.RemoveExecutorFromExecutorPoolUseCase; import ch.unisg.executorpool.domain.ExecutorClass; @@ -11,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; +import java.net.URI; import java.util.Optional; @RestController @@ -21,9 +23,11 @@ public class RemoveExecutorFromExecutorPoolWebController { this.removeExecutorFromExecutorPoolUseCase = removeExecutorFromExecutorPoolUseCase; } - @PostMapping(path = "/executor-pool/RemoveExecutor", consumes = {ExecutorMediaType.EXECUTOR_MEDIA_TYPE}) - public ResponseEntity removeExecutorFromExecutorPool(@RequestBody ExecutorClass executorClass){ - RemoveExecutorFromExecutorPoolCommand command = new RemoveExecutorFromExecutorPoolCommand(executorClass.getExecutorIp(), executorClass.getExecutorPort()); + @PostMapping(path = "/executor-pool/RemoveExecutor", consumes = {ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE}) + public ResponseEntity removeExecutorFromExecutorPool(@RequestBody ExecutorJsonRepresentation executorJsonRepresentation){ + RemoveExecutorFromExecutorPoolCommand command = new RemoveExecutorFromExecutorPoolCommand( + new ExecutorClass.ExecutorUri(URI.create(executorJsonRepresentation.getExecutorUri())) + ); Optional removedExecutor = removeExecutorFromExecutorPoolUseCase.removeExecutorFromExecutorPool(command); if(removedExecutor.isEmpty()){ @@ -31,9 +35,9 @@ public class RemoveExecutorFromExecutorPoolWebController { } HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorMediaType.EXECUTOR_MEDIA_TYPE); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, ExecutorJsonRepresentation.EXECUTOR_MEDIA_TYPE); - return new ResponseEntity<>(ExecutorMediaType.serialize(removedExecutor.get()), responseHeaders, + return new ResponseEntity<>(ExecutorJsonRepresentation.serialize(removedExecutor.get()), responseHeaders, HttpStatus.OK); } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/AddNewExecutorToExecutorPoolCommand.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/AddNewExecutorToExecutorPoolCommand.java index 2682610..ddd7da9 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/AddNewExecutorToExecutorPoolCommand.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/AddNewExecutorToExecutorPoolCommand.java @@ -2,8 +2,7 @@ package ch.unisg.executorpool.application.port.in; import ch.unisg.common.SelfValidating; import ch.unisg.executorpool.domain.ExecutorPool; -import ch.unisg.executorpool.domain.ExecutorClass.ExecutorIp; -import ch.unisg.executorpool.domain.ExecutorClass.ExecutorPort; +import ch.unisg.executorpool.domain.ExecutorClass.ExecutorUri; import ch.unisg.executorpool.domain.ExecutorClass.ExecutorTaskType; import lombok.Value; import javax.validation.constraints.NotNull; @@ -11,17 +10,13 @@ import javax.validation.constraints.NotNull; @Value public class AddNewExecutorToExecutorPoolCommand extends SelfValidating { @NotNull - private final ExecutorIp executorIp; - - @NotNull - private final ExecutorPort executorPort; + private final ExecutorUri executorUri; @NotNull private final ExecutorTaskType executorTaskType; - public AddNewExecutorToExecutorPoolCommand(ExecutorIp executorIp, ExecutorPort executorPort, ExecutorTaskType executorTaskType){ - this.executorIp = executorIp; - this.executorPort = executorPort; + public AddNewExecutorToExecutorPoolCommand(ExecutorUri executorUri, ExecutorTaskType executorTaskType){ + this.executorUri = executorUri; this.executorTaskType = executorTaskType; this.validateSelf(); } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/RemoveExecutorFromExecutorPoolCommand.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/RemoveExecutorFromExecutorPoolCommand.java index 11763a9..162426c 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/RemoveExecutorFromExecutorPoolCommand.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/port/in/RemoveExecutorFromExecutorPoolCommand.java @@ -1,9 +1,7 @@ package ch.unisg.executorpool.application.port.in; -import ch.unisg.executorpool.domain.ExecutorClass; import ch.unisg.common.SelfValidating; -import ch.unisg.executorpool.domain.ExecutorClass.ExecutorIp; -import ch.unisg.executorpool.domain.ExecutorClass.ExecutorPort; +import ch.unisg.executorpool.domain.ExecutorClass.ExecutorUri; import lombok.Value; import javax.validation.constraints.NotNull; @@ -11,14 +9,10 @@ import javax.validation.constraints.NotNull; @Value public class RemoveExecutorFromExecutorPoolCommand extends SelfValidating { @NotNull - private final ExecutorIp executorIp; + private final ExecutorUri executorUri; - @NotNull - private final ExecutorPort executorPort; - - public RemoveExecutorFromExecutorPoolCommand(ExecutorIp executorIp, ExecutorPort executorPort){ - this.executorIp = executorIp; - this.executorPort = executorPort; + public RemoveExecutorFromExecutorPoolCommand(ExecutorUri executorUri){ + this.executorUri = executorUri; this.validateSelf(); } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java index e1ef237..200739b 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/AddNewExecutorToExecutorPoolService.java @@ -20,6 +20,6 @@ public class AddNewExecutorToExecutorPoolService implements AddNewExecutorToExec public ExecutorClass addNewExecutorToExecutorPool(AddNewExecutorToExecutorPoolCommand command){ ExecutorPool executorPool = ExecutorPool.getExecutorPool(); - return executorPool.addNewExecutor(command.getExecutorIp(), command.getExecutorPort(), command.getExecutorTaskType()); + return executorPool.addNewExecutor(command.getExecutorUri(), command.getExecutorTaskType()); } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java index 639ba7f..a606f57 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/application/service/RemoveExecutorFromExecutorPoolService.java @@ -17,6 +17,6 @@ public class RemoveExecutorFromExecutorPoolService implements RemoveExecutorFrom @Override public Optional removeExecutorFromExecutorPool(RemoveExecutorFromExecutorPoolCommand command){ ExecutorPool executorPool = ExecutorPool.getExecutorPool(); - return executorPool.removeExecutorByIpAndPort(command.getExecutorIp(), command.getExecutorPort()); + return executorPool.removeExecutorByIpAndPort(command.getExecutorUri()); } } diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorClass.java b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorClass.java index d1fca00..5da6fe7 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorClass.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorClass.java @@ -3,36 +3,29 @@ package ch.unisg.executorpool.domain; import lombok.Getter; import lombok.Value; +import java.net.URI; + public class ExecutorClass { @Getter - private final ExecutorIp executorIp; - - @Getter - private final ExecutorPort executorPort; + private final ExecutorUri executorUri; @Getter private final ExecutorTaskType executorTaskType; - public ExecutorClass(ExecutorIp executorIp, ExecutorPort executorPort, ExecutorTaskType executorTaskType){ - this.executorIp = executorIp; - this.executorPort = executorPort; + public ExecutorClass(ExecutorUri executorUri, ExecutorTaskType executorTaskType){ + this.executorUri = executorUri; this.executorTaskType = executorTaskType; } - protected static ExecutorClass createExecutorClass(ExecutorIp executorIp, ExecutorPort executorPort, ExecutorTaskType executorTaskType){ - System.out.println("New Task: " + executorIp.getValue() + " " + executorPort.getValue() + " " + executorTaskType.getValue()); - return new ExecutorClass(executorIp, executorPort, executorTaskType); + protected static ExecutorClass createExecutorClass(ExecutorUri executorUri, ExecutorTaskType executorTaskType){ + System.out.println("New Executor: " + executorUri.value.toString() + " " + executorTaskType.getValue()); + return new ExecutorClass(executorUri, executorTaskType); } @Value - public static class ExecutorIp { - private String value; - } - - @Value - public static class ExecutorPort { - private String value; + public static class ExecutorUri { + private URI value; } @Value diff --git a/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorPool.java b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorPool.java index dd5375b..0ca0d5e 100644 --- a/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorPool.java +++ b/executor-pool/src/main/java/ch/unisg/executorpool/domain/ExecutorPool.java @@ -1,5 +1,8 @@ package ch.unisg.executorpool.domain; +import ch.unisg.executorpool.domain.ExecutorClass.ExecutorUri; +import ch.unisg.executorpool.domain.ExecutorClass.ExecutorTaskType; + import lombok.Getter; import lombok.Value; @@ -20,19 +23,17 @@ public class ExecutorPool { public static ExecutorPool getExecutorPool() { return executorPool; } - public ExecutorClass addNewExecutor(ExecutorClass.ExecutorIp executorIp, ExecutorClass.ExecutorPort executorPort, ExecutorClass.ExecutorTaskType executorTaskType){ - ExecutorClass newExecutor = ExecutorClass.createExecutorClass(executorIp, executorPort, executorTaskType); + public ExecutorClass addNewExecutor(ExecutorUri executorUri, ExecutorTaskType executorTaskType){ + ExecutorClass newExecutor = ExecutorClass.createExecutorClass(executorUri, executorTaskType); listOfExecutors.value.add(newExecutor); System.out.println("Number of executors: " + listOfExecutors.value.size()); return newExecutor; } - public Optional getExecutorByIpAndPort(ExecutorClass.ExecutorIp executorIp, ExecutorClass.ExecutorPort executorPort){ + public Optional getExecutorByUri(ExecutorUri executorUri){ for (ExecutorClass executor : listOfExecutors.value ) { - // TODO can this be simplified by overwriting equals()? - if(executor.getExecutorIp().getValue().equalsIgnoreCase(executorIp.getValue()) && - executor.getExecutorPort().getValue().equalsIgnoreCase(executorPort.getValue())){ + if(executor.getExecutorUri().getValue().equals(executorUri)){ return Optional.of(executor); } } @@ -54,11 +55,10 @@ public class ExecutorPool { return matchedExecutors; } - public Optional removeExecutorByIpAndPort(ExecutorClass.ExecutorIp executorIp, ExecutorClass.ExecutorPort executorPort){ + public Optional removeExecutorByIpAndPort(ExecutorUri executorUri){ for (ExecutorClass executor : listOfExecutors.value ) { // TODO can this be simplified by overwriting equals()? - if(executor.getExecutorIp().getValue().equalsIgnoreCase(executorIp.getValue()) && - executor.getExecutorPort().getValue().equalsIgnoreCase(executorPort.getValue())){ + if(executor.getExecutorUri().getValue().equals(executorUri.getValue())){ listOfExecutors.value.remove(executor); return Optional.of(executor); } From 74a51cfcf681f8acd804eef2b24dc341e61e419b Mon Sep 17 00:00:00 2001 From: "julius.lautz" Date: Wed, 10 Nov 2021 11:03:57 +0000 Subject: [PATCH 20/40] changed deadline to timestamp and the logic to schedule an auction --- .../application/port/in/LaunchAuctionCommand.java | 5 +++-- .../application/service/StartAuctionService.java | 3 ++- .../java/ch/unisg/tapas/auctionhouse/domain/Auction.java | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java index 626fa49..37eb5db 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/port/in/LaunchAuctionCommand.java @@ -1,10 +1,11 @@ package ch.unisg.tapas.auctionhouse.application.port.in; import ch.unisg.tapas.auctionhouse.domain.Auction; -import ch.unisg.tapas.common.SelfValidating; +import ch.unisg.common.SelfValidating; +import lombok.NonNull; import lombok.Value; -import javax.validation.constraints.NotNull; +import javax.validation.constraint.NotNull; /** * Command for launching an auction in this auction house. diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java index 42c6e37..b9d9d3d 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/application/service/StartAuctionService.java @@ -2,6 +2,7 @@ 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.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.*; @@ -63,7 +64,7 @@ public class StartAuctionService implements LaunchAuctionUseCase { auctions.addAuction(auction); // Schedule the closing of the auction at the deadline - service.schedule(new CloseAuctionTask(auction.getAuctionId()), deadline.getValue(), + service.schedule(new CloseAuctionTask(auction.getAuctionId()), deadline.getValue().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS); // Publish an auction started event diff --git a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java index 3e51ef7..c6d9333 100644 --- a/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java +++ b/tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/domain/Auction.java @@ -4,6 +4,7 @@ import lombok.Getter; import lombok.Value; import java.net.URI; +import java.sql.Timestamp; import java.util.*; /** @@ -166,6 +167,6 @@ public class Auction { @Value public static class AuctionDeadline { - int value; + Timestamp value; } } From 18fdf819f1d45e5cc1d45a5ec41c557b12e5ad24 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 10 Nov 2021 15:14:41 +0100 Subject: [PATCH 21/40] First implementation of WebSub --- mocks/README.md | 29 +++++++ mocks/auction-house/auctions.js | 39 +++++++++ mocks/auction-house/publisher.js | 17 ++++ mocks/auction-house/subscriber.js | 42 +++++++++ tapas-auction-house/pom.xml | 11 +++ .../tapas/TapasAuctionHouseApplication.java | 11 ++- .../common/clients/WebSubSubscriber.java | 85 +++++++++++++++++++ ...tionStartedEventListenerWebSubAdapter.java | 21 ++++- .../websub/ValidateIntentWebSubAdapter.java | 36 ++++++++ ...blishAuctionStartedEventWebSubAdapter.java | 38 ++++++++- .../src/main/resources/application.properties | 4 + 11 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 mocks/README.md create mode 100644 mocks/auction-house/auctions.js create mode 100644 mocks/auction-house/publisher.js create mode 100644 mocks/auction-house/subscriber.js create mode 100644 tapas-auction-house/src/main/java/ch/unisg/tapas/auctionhouse/adapter/in/messaging/websub/ValidateIntentWebSubAdapter.java diff --git a/mocks/README.md b/mocks/README.md new file mode 100644 index 0000000..7135682 --- /dev/null +++ b/mocks/README.md @@ -0,0 +1,29 @@ +In this directory are some files to mock an auction house to test WebSub local. + +To run a local WebSubHub instance + +1. Start a mongodb in docker: + +- docker run -d -p 27017:27017 -p 28017:28017 -e AUTH=no tutum/mongodb + +2. Install a local hub + +- yarn global add websub-hub + +3. Run the hub localy + +- websub-hub -l info -m mongodb://localhost:27017/hub + +Create an example subscription + +- node auction-house/subscriber.js + +Create an example auctionhouse + +- node auction-house/auctions.js + +Publish to the hub + +- node auction-house/publisher.js + +Mostly inspired by: https://github.com/hemerajs/websub-hub diff --git a/mocks/auction-house/auctions.js b/mocks/auction-house/auctions.js new file mode 100644 index 0000000..68a6aa5 --- /dev/null +++ b/mocks/auction-house/auctions.js @@ -0,0 +1,39 @@ +// Require the framework and instantiate it +const fastify = require('fastify')({ logger: true }) + +// Declare a route +fastify.get('/auctions', async (request, reply) => { + console.log('content provided') + + return [ + { + id: '2', + content_text: 'This is a second item.', + url: 'https://example.org/second-item' + }, + { + id: '1', + content_html: '