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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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