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

@@ -11,61 +11,159 @@ with default editor settings. EditorConfig is supported out-of-the-box by the In
consistent code styles, we recommend to reuse this editor configuration file in all your services.
## HTTP API Overview
The code we provide includes a minimalistic HTTP API for (i) creating a new task and (ii) retrieving
the representation of a task.
The code we provide includes a minimalistic uniform HTTP API for (i) creating a new task, (ii) retrieving
a representation of the current state of a task, and (iii) patching the representation of a task, which
is mapped to a domain/integration event.
The representations exchanged with the API use two media types:
* a JSON-based format for task with the media type `application/task+json`; this media type is defined
in the context of our project, but could be [registered with IANA](https://www.iana.org/assignments/media-types)
to promote interoperability (see
[TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java)
for more details)
* the [JSON Patch](http://jsonpatch.com/) format with the registered media type `application/json-patch+json`, which is also a
JSON-based format (see sample HTTP requests below).
For further developing and working with your HTTP API, we recommend to use [Postman](https://www.postman.com/).
### Creating a new task
A new task is created via an `HTTP POST` request to the `/tasks/` endpoint. The body of the request
must include a JSON payload with the content type `application/json` and two required fields:
must include a representation of the task to be created using the content type `application/task+json`
defined in the context of this project. A valid representation must include at least two required fields
(see [TaskJsonRepresentation](src/main/java/ch/unisg/tapastasks/tasks/adapter/in/formats/TaskJsonRepresentation.java)
for more details):
* `taskName`: a string that represents the name of the task to be created
* `taskType`: a string that represents the type of the task to be created
A sample HTTP request with `curl`:
```shell
curl -i --location --request POST 'http://localhost:8081/tasks/' --header 'Content-Type: application/json' --data-raw '{
"taskName" : "task1",
"taskType" : "type1"
curl -i --location --request POST 'http://localhost:8081/tasks/' \
--header 'Content-Type: application/task+json' \
--data-raw '{
"taskName" : "task1",
"taskType" : "computation",
"originalTaskUri" : "http://example.org",
"inputData" : "1+1"
}'
HTTP/1.1 201
Content-Type: application/json
Content-Length: 142
Date: Sun, 03 Oct 2021 17:25:32 GMT
Location: http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354
Content-Type: application/task+json
Content-Length: 170
Date: Sun, 17 Oct 2021 21:03:34 GMT
{
"taskType" : "type1",
"taskState" : "OPEN",
"taskListName" : "tapas-tasks-tutors",
"taskName" : "task1",
"taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0"
"taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskName":"task1",
"taskType":"computation",
"taskStatus":"OPEN",
"originalTaskUri":"http://example.org",
"inputData":"1+1"
}
```
If the task is created successfuly, a `201 Created` status code is returned together with a JSON
representation of the created task. The representation includes, among others, a _universally unique
identifier (UUID)_ for the newly created task (`taskId`).
If the task is created successfuly, a `201 Created` status code is returned together with a
representation of the created task. The response also includes a `Location` header filed that points
to the URI of the newly created task.
### Retrieving a task
The representation of a task is retrieved via an `HTTP GET` request to the `/tasks/<task-UUID>` endpoint.
The representation of a task is retrieved via an `HTTP GET` request to the URI of task.
A sample HTTP request with `curl`:
```shell
curl -i --location --request GET 'http://localhost:8081/tasks/53cb19d6-2d9b-486f-98c7-c96c93b037f0'
curl -i --location --request GET 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354'
HTTP/1.1 200
Content-Type: application/json
Content-Length: 142
Date: Sun, 03 Oct 2021 17:27:06 GMT
Content-Type: application/task+json
Content-Length: 170
Date: Sun, 17 Oct 2021 21:07:04 GMT
{
"taskType" : "type1",
"taskState" : "OPEN",
"taskListName" : "tapas-tasks-tutors",
"taskName" : "task1",
"taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0"
"taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskName":"task1",
"taskType":"computation",
"taskStatus":"OPEN",
"originalTaskUri":"http://example.org",
"inputData":"1+1"
}
```
### Patching a task
REST emphasizes the generality of interfaces to promote uniform interaction. For instance, we can use
the `HTTP PATCH` method to implement fine-grained updates to the representational state of a task, which
may translate to various domain/integration events. However, to conform to the uniform interface
contraint in REST, any such updates have to rely on standard knowledge — and thus to hide away the
implementation details of our service.
In addition to the `application/task+json` media type we defined for our uniform HTTP API, a standard
representation format we can use to specify fine-grained updates to the representation of tasks
is [JSON Patch](http://jsonpatch.com/). In what follow, we provide a few examples of `HTTP PATCH` requests.
For further details on the JSON Patch format, see also [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902)).
#### Changing the status of a task from OPEN to ASSIGNED
Sample HTTP request that assigns the previously created task to group `tapas-group1`:
```shell
curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \
--header 'Content-Type: application/json-patch+json' \
--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "ASSIGNED" },
{"op" : "add", "path": "/serviceProvider", "value" : "tapas-group1" } ]'
HTTP/1.1 200
Content-Type: application/task+json
Content-Length: 207
Date: Sun, 17 Oct 2021 21:20:58 GMT
{
"taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskName":"task1",
"taskType":"computation",
"taskStatus":"ASSIGNED",
"originalTaskUri":"http://example.org",
"serviceProvider":"tapas-group1",
"inputData":"1+1"
}
```
In this example, the requested patch includes two JSON Patch operations:
* an operation to `replace` the `taskStatus` already in the task's representation with the value `ASSIGNED`
* an operation to `add` to the task's representation a `serviceProvider` with the value `tapas-group1`
Internally, this request is mapped to a
[TaskAssignedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskAssignedEvent.java).
The HTTP response returns a `200 OK` status code together with the updated representation of the task.
#### Changing the status of a task from to EXECUTED
Sample HTTP request that changes the status of the task to `EXECUTED` and adds an output result:
```shell
curl -i --location --request PATCH 'http://localhost:8081/tasks/cef2fa9d-367b-4e7f-bf06-3b1fea35f354' \
--header 'Content-Type: application/json-patch+json' \
--data-raw '[ {"op" : "replace", "path": "/taskStatus", "value" : "EXECUTED" },
{"op" : "add", "path": "/outputData", "value" : "2" } ]'
HTTP/1.1 200
Content-Type: application/task+json
Content-Length: 224
Date: Sun, 17 Oct 2021 21:32:25 GMT
{
"taskId":"cef2fa9d-367b-4e7f-bf06-3b1fea35f354",
"taskName":"task1",
"taskType":"computation",
"taskStatus":"EXECUTED",
"originalTaskUri":"http://example.org",
"serviceProvider":"tapas-group1",
"inputData":"1+1",
"outputData":"2"
}
```
Internally, this request is mapped to a
[TaskExecutedEvent](src/main/java/ch/unisg/tapastasks/tasks/application/port/in/TaskExecutedEvent.java).
The HTTP response returns a `200 OK` status code together with the updated representation of the task.

View File

@@ -16,6 +16,12 @@
<properties>
<java.version>11</java.version>
</properties>
<repositories>
<repository>
<id>Eclipse Paho Repo</id>
<url>https://repo.eclipse.org/content/repositories/paho-releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -49,17 +55,16 @@
<version>1.1.0.Final</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
<dependency>
<groupId>com.github.java-json-tools</groupId>
<artifactId>json-patch</artifactId>
<version>1.13</version>
</dependency>
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>

View File

@@ -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);
}

View File

@@ -0,0 +1,102 @@
package ch.unisg.tapastasks.tasks.adapter.in.formats;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
/**
* This class is used to process JSON Patch operations for tasks: given a
* <a href="http://jsonpatch.com/">JSON Patch</a> for updating the representational state of a task,
* this class provides methods for extracting various operations of interest for our domain (e.g.,
* changing the status of a task).
*/
public class TaskJsonPatchRepresentation {
public static final String MEDIA_TYPE = "application/json-patch+json";
private final JsonNode patch;
/**
* Constructs the JSON Patch representation.
*
* @param patch a JSON Patch as JsonNode
*/
public TaskJsonPatchRepresentation(JsonNode patch) {
this.patch = patch;
}
/**
* Extracts the first task status replaced in this patch.
*
* @return the first task status changed in this patch or an empty {@link Optional} if none is
* found
*/
public Optional<Task.Status> extractFirstTaskStatusChange() {
Optional<JsonNode> status = extractFirst(node ->
isPatchReplaceOperation(node) && hasPath(node, "/taskStatus")
);
if (status.isPresent()) {
String taskStatus = status.get().get("value").asText();
return Optional.of(Task.Status.valueOf(taskStatus));
}
return Optional.empty();
}
/**
* Extracts the first service provider added or replaced in this patch.
*
* @return the first service provider changed in this patch or an empty {@link Optional} if none
* is found
*/
public Optional<Task.ServiceProvider> extractFirstServiceProviderChange() {
Optional<JsonNode> serviceProvider = extractFirst(node ->
(isPatchReplaceOperation(node) || isPatchAddOperation(node))
&& hasPath(node, "/serviceProvider")
);
return (serviceProvider.isEmpty()) ? Optional.empty()
: Optional.of(new Task.ServiceProvider(serviceProvider.get().get("value").asText()));
}
/**
* Extracts the first output data addition in this patch.
*
* @return the output data added in this patch or an empty {@link Optional} if none is found
*/
public Optional<Task.OutputData> extractFirstOutputDataAddition() {
Optional<JsonNode> output = extractFirst(node ->
isPatchAddOperation(node) && hasPath(node, "/outputData")
);
return (output.isEmpty()) ? Optional.empty()
: Optional.of(new Task.OutputData(output.get().get("value").asText()));
}
private Optional<JsonNode> extractFirst(Predicate<? super JsonNode> predicate) {
Stream<JsonNode> stream = StreamSupport.stream(patch.spliterator(), false);
return stream.filter(predicate).findFirst();
}
private boolean isPatchAddOperation(JsonNode node) {
return isPatchOperationOfType(node, "add");
}
private boolean isPatchReplaceOperation(JsonNode node) {
return isPatchOperationOfType(node, "replace");
}
private boolean isPatchOperationOfType(JsonNode node, String operation) {
return node.isObject() && node.get("op") != null
&& node.get("op").asText().equalsIgnoreCase(operation);
}
private boolean hasPath(JsonNode node, String path) {
return node.isObject() && node.get("path") != null
&& node.get("path").asText().equalsIgnoreCase(path);
}
}

View File

@@ -0,0 +1,115 @@
package ch.unisg.tapastasks.tasks.adapter.in.formats;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Getter;
import lombok.Setter;
/**
* This class is used to expose and consume representations of tasks through the HTTP interface. The
* representations conform to the custom JSON-based media type "application/task+json". The media type
* is just an identifier and can be registered with
* <a href="https://www.iana.org/assignments/media-types/">IANA</a> to promote interoperability.
*/
final public class TaskJsonRepresentation {
// The media type used for this task representation format
public static final String MEDIA_TYPE = "application/task+json";
// A task identifier specific to our implementation (e.g., a UUID). This identifier is then used
// to generate the task's URI. URIs are standard uniform identifiers and use a universal syntax
// that can be referenced (and dereferenced) independent of context. In our uniform HTTP API,
// we identify tasks via URIs and not implementation-specific identifiers.
@Getter @Setter
private String taskId;
// A string that represents the task's name
@Getter
private final String taskName;
// A string that identifies the task's type. This string could also be a URI (e.g., defined in some
// Web ontology, as we shall see later in the course), but it's not constrained to be a URI.
// The task's type can be used to assign executors to tasks, to decide what tasks to bid for, etc.
@Getter
private final String taskType;
// The task's status: OPEN, ASSIGNED, RUNNING, or EXECUTED (see Task.Status)
@Getter @Setter
private String taskStatus;
// If this task is a delegated task (i.e., a shadow of another task), this URI points to the
// original task. Because URIs are standard and uniform, we can just dereference this URI to
// retrieve a representation of the original task.
@Getter @Setter
private String originalTaskUri;
// The service provider who executes this task. The service provider is a any string that identifies
// a TAPAS group (e.g., tapas-group1). This identifier could also be a URI (if we have a good reason
// for it), but it's not constrained to be a URI.
@Getter @Setter
private String serviceProvider;
// A string that provides domain-specific input data for this task. In the context of this project,
// we can parse and interpret the input data based on the task's type.
@Getter @Setter
private String inputData;
// A string that provides domain-specific output data for this task. In the context of this project,
// we can parse and interpret the output data based on the task's type.
@Getter @Setter
private String outputData;
/**
* Instantiate a task representation with a task name and type.
*
* @param taskName string that represents the task's name
* @param taskType string that represents the task's type
*/
public TaskJsonRepresentation(String taskName, String taskType) {
this.taskName = taskName;
this.taskType = taskType;
this.taskStatus = null;
this.originalTaskUri = null;
this.serviceProvider = null;
this.inputData = null;
this.outputData = null;
}
/**
* Instantiate a task representation from a domain concept.
*
* @param task the task
*/
public TaskJsonRepresentation(Task task) {
this(task.getTaskName().getValue(), task.getTaskType().getValue());
this.taskId = task.getTaskId().getValue();
this.taskStatus = task.getTaskStatus().getValue().name();
this.originalTaskUri = (task.getOriginalTaskUri() == null) ?
null : task.getOriginalTaskUri().getValue();
this.serviceProvider = (task.getProvider() == null) ? null : task.getProvider().getValue();
this.inputData = (task.getInputData() == null) ? null : task.getInputData().getValue();
this.outputData = (task.getOutputData() == null) ? null : task.getOutputData().getValue();
}
/**
* Convenience method used to serialize a task provided as a domain concept in the format exposed
* through the uniform HTTP API.
*
* @param task the task as defined in the domain
* @return a string serialization using the JSON-based representation format defined for tasks
* @throws JsonProcessingException if a runtime exception occurs during object serialization
*/
public static String serialize(Task task) throws JsonProcessingException {
TaskJsonRepresentation representation = new TaskJsonRepresentation(task);
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper.writeValueAsString(representation);
}
}

View File

@@ -0,0 +1,3 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging;
public class UnknownEventException extends RuntimeException { }

View File

@@ -0,0 +1,39 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.application.handler.TaskAssignedHandler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskAssignedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.Task.TaskId;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
/**
* Listener for task assigned events. A task assigned event corresponds to a JSON Patch that attempts
* to change the task's status to ASSIGNED and may also add/replace a service provider (i.e., to what
* group the task was assigned). This implementation does not impose that a task assigned event
* includes the service provider (i.e., can be null).
*
* See also {@link TaskAssignedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
*/
public class TaskAssignedEventListenerHttpAdapter extends TaskEventListener {
/**
* Handles the task assigned event.
*
* @param taskId the identifier of the task for which an event was received
* @param payload the JSON Patch payload of the HTTP PATCH request received for this task
* @return
*/
public Task handleTaskEvent(String taskId, JsonNode payload) {
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
TaskAssignedEvent taskAssignedEvent = new TaskAssignedEvent(new TaskId(taskId), serviceProvider);
TaskAssignedEventHandler taskAssignedEventHandler = new TaskAssignedHandler();
return taskAssignedEventHandler.handleTaskAssigned(taskAssignedEvent);
}
}

View File

@@ -0,0 +1,103 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.adapter.in.messaging.UnknownEventException;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.TaskNotFoundException;
import com.fasterxml.jackson.databind.JsonNode;
import com.github.fge.jsonpatch.JsonPatch;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.io.IOException;
import java.util.Optional;
/**
* This REST Controller handles HTTP PATCH requests for updating the representational state of Task
* resources. Each request to update the representational state of a Task resource can correspond to
* at most one domain/integration event. Request payloads use the
* <a href="http://jsonpatch.com/">JSON PATCH</a> format and media type.
*
* A JSON Patch can contain multiple operations (e.g., add, remove, replace) for updating various
* parts of a task's representations. One or more JSON Patch operations can represent a domain/integration
* event. Therefore, the events can only be determined by inspecting the requested patch (e.g., a request
* to change a task's status from RUNNING to EXECUTED). This class is responsible to inspect requested
* patches, identify events, and to route them to appropriate listeners.
*
* For more details on JSON Patch, see: <a href="http://jsonpatch.com/">http://jsonpatch.com/</a>
* For some sample HTTP requests, see the README.
*/
@RestController
public class TaskEventHttpDispatcher {
// The standard media type for JSON Patch registered with IANA
// See: https://www.iana.org/assignments/media-types/application/json-patch+json
private final static String JSON_PATCH_MEDIA_TYPE = "application/json-patch+json";
/**
* Handles HTTP PATCH requests with a JSON Patch payload. Routes the requests based on the
* the operations requested in the patch. In this implementation, one HTTP Patch request is
* mapped to at most one domain event.
*
* @param taskId the local (i.e., implementation-specific) identifier of the task to the patched;
* this identifier is extracted from the task's URI
* @param payload the reuqested patch for this task
* @return 200 OK and a representation of the task after processing the event; 404 Not Found if
* the request URI does not match any task; 400 Bad Request if the request is invalid
*/
@PatchMapping(path = "/tasks/{taskId}", consumes = {JSON_PATCH_MEDIA_TYPE})
public ResponseEntity<String> dispatchTaskEvents(@PathVariable("taskId") String taskId,
@RequestBody JsonNode payload) {
try {
// Throw an exception if the JSON Patch format is invalid. This call is only used to
// validate the JSON PATCH syntax.
JsonPatch.fromJson(payload);
// Check for known events and route the events to appropriate listeners
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.Status> status = representation.extractFirstTaskStatusChange();
TaskEventListener listener = null;
// Route events related to task status changes
if (status.isPresent()) {
switch (status.get()) {
case ASSIGNED:
listener = new TaskAssignedEventListenerHttpAdapter();
break;
case RUNNING:
listener = new TaskStartedEventListenerHttpAdapter();
break;
case EXECUTED:
listener = new TaskExecutedEventListenerHttpAdapter();
break;
}
}
if (listener == null) {
// The HTTP PATCH request is valid, but the patch does not match any known event
throw new UnknownEventException();
}
Task task = listener.handleTaskEvent(taskId, payload);
// Add the content type as a response header
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
return new ResponseEntity<>(TaskJsonRepresentation.serialize(task), responseHeaders,
HttpStatus.OK);
} catch (TaskNotFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
} catch (IOException | RuntimeException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,34 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.application.handler.TaskExecutedHandler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskExecutedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
/**
* Listener for task executed events. A task executed event corresponds to a JSON Patch that attempts
* to change the task's status to EXECUTED, may add/replace a service provider, and may also add an
* output result. This implementation does not impose that a task executed event includes either the
* service provider or an output result (i.e., both can be null).
*
* See also {@link TaskExecutedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
*/
public class TaskExecutedEventListenerHttpAdapter extends TaskEventListener {
public Task handleTaskEvent(String taskId, JsonNode payload) {
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
Optional<Task.OutputData> outputData = representation.extractFirstOutputDataAddition();
TaskExecutedEvent taskExecutedEvent = new TaskExecutedEvent(new Task.TaskId(taskId),
serviceProvider, outputData);
TaskExecutedEventHandler taskExecutedEventHandler = new TaskExecutedHandler();
return taskExecutedEventHandler.handleTaskExecuted(taskExecutedEvent);
}
}

View File

@@ -0,0 +1,32 @@
package ch.unisg.tapastasks.tasks.adapter.in.messaging.http;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonPatchRepresentation;
import ch.unisg.tapastasks.tasks.application.handler.TaskStartedHandler;
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEvent;
import ch.unisg.tapastasks.tasks.application.port.in.TaskStartedEventHandler;
import ch.unisg.tapastasks.tasks.domain.Task;
import ch.unisg.tapastasks.tasks.domain.Task.TaskId;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.Optional;
/**
* Listener for task started events. A task started event corresponds to a JSON Patch that attempts
* to change the task's status to RUNNING and may also add/replace a service provider. This
* implementation does not impose that a task started event includes the service provider (i.e.,
* can be null).
*
* See also {@link TaskStartedEvent}, {@link Task}, and {@link TaskEventHttpDispatcher}.
*/
public class TaskStartedEventListenerHttpAdapter extends TaskEventListener {
public Task handleTaskEvent(String taskId, JsonNode payload) {
TaskJsonPatchRepresentation representation = new TaskJsonPatchRepresentation(payload);
Optional<Task.ServiceProvider> serviceProvider = representation.extractFirstServiceProviderChange();
TaskStartedEvent taskStartedEvent = new TaskStartedEvent(new TaskId(taskId), serviceProvider);
TaskStartedEventHandler taskStartedEventHandler = new TaskStartedHandler();
return taskStartedEventHandler.handleTaskStarted(taskStartedEvent);
}
}

View File

@@ -1,8 +1,12 @@
package ch.unisg.tapastasks.tasks.adapter.in.web;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand;
import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -12,29 +16,67 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import javax.validation.ConstraintViolationException;
import java.util.Optional;
/**
* Controller that handles HTTP requests for creating new tasks. This controller implements the
* {@link AddNewTaskToTaskListUseCase} use case using the {@link AddNewTaskToTaskListCommand}.
*
* A new task is created via an HTTP POST request to the /tasks/ endpoint. The body of the request
* contains a JSON-based representation with the "application/task+json" media type defined for this
* project. This custom media type allows to capture the semantics of our JSON representations for
* tasks.
*
* If the request is successful, the controller returns an HTTP 201 Created status code and a
* representation of the created task with Content-Type "application/task+json". The HTTP response
* also include a Location header field that points to the URI of the created task.
*/
@RestController
public class AddNewTaskToTaskListWebController {
private final AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase;
// Used to retrieve properties from application.properties
@Autowired
private Environment environment;
public AddNewTaskToTaskListWebController(AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase) {
this.addNewTaskToTaskListUseCase = addNewTaskToTaskListUseCase;
}
@PostMapping(path = "/tasks/", consumes = {TaskMediaType.TASK_MEDIA_TYPE})
public ResponseEntity<String> addNewTaskTaskToTaskList(@RequestBody Task task) {
@PostMapping(path = "/tasks/", consumes = {TaskJsonRepresentation.MEDIA_TYPE})
public ResponseEntity<String> addNewTaskTaskToTaskList(@RequestBody TaskJsonRepresentation payload) {
try {
AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(
task.getTaskName(), task.getTaskType()
);
Task.TaskName taskName = new Task.TaskName(payload.getTaskName());
Task.TaskType taskType = new Task.TaskType(payload.getTaskType());
Task newTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command);
// If the created task is a delegated task, the representation contains a URI reference
// to the original task
Optional<Task.OriginalTaskUri> originalTaskUriOptional =
(payload.getOriginalTaskUri() == null) ? Optional.empty()
: Optional.of(new Task.OriginalTaskUri(payload.getOriginalTaskUri()));
AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand(taskName, taskType,
originalTaskUriOptional);
Task createdTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command);
// When creating a task, the task's representation may include optional input data
if (payload.getInputData() != null) {
createdTask.setInputData(new Task.InputData(payload.getInputData()));
}
// Add the content type as a response header
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE);
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
// Construct and advertise the URI of the newly created task; we retrieve the base URI
// from the application.properties file
responseHeaders.add(HttpHeaders.LOCATION, environment.getProperty("baseuri")
+ "tasks/" + createdTask.getTaskId().getValue());
return new ResponseEntity<>(TaskMediaType.serialize(newTask), responseHeaders, HttpStatus.CREATED);
return new ResponseEntity<>(TaskJsonRepresentation.serialize(createdTask), responseHeaders,
HttpStatus.CREATED);
} catch (JsonProcessingException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
} catch (ConstraintViolationException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}

View File

@@ -1,8 +1,10 @@
package ch.unisg.tapastasks.tasks.adapter.in.web;
import ch.unisg.tapastasks.tasks.adapter.in.formats.TaskJsonRepresentation;
import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListQuery;
import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase;
import ch.unisg.tapastasks.tasks.domain.Task;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -11,6 +13,11 @@ import org.springframework.web.server.ResponseStatusException;
import java.util.Optional;
/**
* Controller that handles HTTP GET requests for retrieving tasks. This controller implements the
* {@link RetrieveTaskFromTaskListUseCase} use case using the {@link RetrieveTaskFromTaskListQuery}
* query.
*/
@RestController
public class RetrieveTaskFromTaskListWebController {
private final RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase;
@@ -19,10 +26,17 @@ public class RetrieveTaskFromTaskListWebController {
this.retrieveTaskFromTaskListUseCase = retrieveTaskFromTaskListUseCase;
}
/**
* Retrieves a representation of task. Returns HTTP 200 OK if the request is successful with a
* representation of the task using the Content-Type "applicatoin/task+json".
*
* @param taskId the local identifier of the requested task (extracted from the task's URI)
* @return a representation of the task if the task exists
*/
@GetMapping(path = "/tasks/{taskId}")
public ResponseEntity<String> retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) {
RetrieveTaskFromTaskListQuery command = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId));
Optional<Task> updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(command);
RetrieveTaskFromTaskListQuery query = new RetrieveTaskFromTaskListQuery(new Task.TaskId(taskId));
Optional<Task> updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(query);
// Check if the task with the given identifier exists
if (updatedTaskOpt.isEmpty()) {
@@ -30,11 +44,16 @@ public class RetrieveTaskFromTaskListWebController {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
// Add the content type as a response header
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE);
try {
String taskRepresentation = TaskJsonRepresentation.serialize(updatedTaskOpt.get());
return new ResponseEntity<>(TaskMediaType.serialize(updatedTaskOpt.get()), responseHeaders,
HttpStatus.OK);
// Add the content type as a response header
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskJsonRepresentation.MEDIA_TYPE);
return new ResponseEntity<>(taskRepresentation, responseHeaders, HttpStatus.OK);
} catch (JsonProcessingException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,30 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task.TaskType;
import ch.unisg.tapastasks.tasks.domain.Task.TaskName;
import ch.unisg.tapastasks.tasks.domain.Task;
import lombok.Getter;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value
public class AddNewTaskToTaskListCommand extends SelfValidating<AddNewTaskToTaskListCommand> {
@NotNull
private final TaskName taskName;
private final Task.TaskName taskName;
@NotNull
private final TaskType taskType;
private final Task.TaskType taskType;
public AddNewTaskToTaskListCommand(TaskName taskName, TaskType taskType) {
@Getter
private final Optional<Task.OriginalTaskUri> originalTaskUri;
public AddNewTaskToTaskListCommand(Task.TaskName taskName, Task.TaskType taskType,
Optional<Task.OriginalTaskUri> originalTaskUri) {
this.taskName = taskName;
this.taskType = taskType;
this.originalTaskUri = originalTaskUri;
this.validateSelf();
}
}

View File

@@ -5,5 +5,5 @@ import ch.unisg.tapastasks.tasks.domain.Task;
import java.util.Optional;
public interface RetrieveTaskFromTaskListUseCase {
Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command);
Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query);
}

View File

@@ -0,0 +1,25 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task;
import lombok.Getter;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value
public class TaskAssignedEvent extends SelfValidating<TaskAssignedEvent> {
@NotNull
private final Task.TaskId taskId;
@Getter
private final Optional<Task.ServiceProvider> serviceProvider;
public TaskAssignedEvent(Task.TaskId taskId, Optional<Task.ServiceProvider> serviceProvider) {
this.taskId = taskId;
this.serviceProvider = serviceProvider;
this.validateSelf();
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,34 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task.*;
import lombok.Getter;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value
public class TaskExecutedEvent extends SelfValidating<TaskExecutedEvent> {
@NotNull
private final TaskId taskId;
@Getter
private final Optional<ServiceProvider> serviceProvider;
@Getter
private final Optional<OutputData> outputData;
public TaskExecutedEvent(TaskId taskId, Optional<ServiceProvider> serviceProvider,
Optional<OutputData> outputData) {
this.taskId = taskId;
this.serviceProvider = serviceProvider;
this.outputData = outputData;
this.validateSelf();
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,28 @@
package ch.unisg.tapastasks.tasks.application.port.in;
import ch.unisg.tapastasks.common.SelfValidating;
import ch.unisg.tapastasks.tasks.domain.Task;
import lombok.Getter;
import lombok.Value;
import javax.validation.constraints.NotNull;
import java.util.Optional;
@Value
public class TaskStartedEvent extends SelfValidating<TaskStartedEvent> {
@NotNull
private final Task.TaskId taskId;
@Getter
private final Optional<Task.ServiceProvider> serviceProvider;
public TaskStartedEvent(Task.TaskId taskId, Optional<Task.ServiceProvider> serviceProvider) {
this.taskId = taskId;
this.serviceProvider = serviceProvider;
this.validateSelf();
}
}

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -15,8 +15,8 @@ import java.util.Optional;
@Transactional
public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase {
@Override
public Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery command) {
public Optional<Task> retrieveTaskFromTaskList(RetrieveTaskFromTaskListQuery query) {
TaskList taskList = TaskList.getTapasTaskList();
return taskList.retrieveTaskById(command.getTaskId());
return taskList.retrieveTaskById(query.getTaskId());
}
}

View File

@@ -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;
}
}

View File

@@ -3,7 +3,6 @@ package ch.unisg.tapastasks.tasks.domain;
import lombok.Getter;
import lombok.Value;
import javax.swing.text.html.Option;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
@@ -20,7 +19,7 @@ public class TaskList {
//Note: We do not care about the management of task lists, there is only one within this service
//--> using the Singleton pattern here to make lives easy; we will later load it from a repo
//TODO change "tutors" to your group name ("groupx")
//TODO: change "tutors" to your group name ("groupx")
private static final TaskList taskList = new TaskList(new TaskListName("tapas-tasks-tutors"));
private TaskList(TaskListName taskListName) {
@@ -35,14 +34,27 @@ public class TaskList {
//Only the aggregate root is allowed to create new tasks and add them to the task list.
//Note: Here we could add some sophisticated invariants/business rules that the aggregate root checks
public Task addNewTaskWithNameAndType(Task.TaskName name, Task.TaskType type) {
Task newTask = Task.createTaskWithNameAndType(name,type);
listOfTasks.value.add(newTask);
//This is a simple debug message to see that the task list is growing with each new request
System.out.println("Number of tasks: "+listOfTasks.value.size());
Task newTask = Task.createTaskWithNameAndType(name, type);
this.addNewTaskToList(newTask);
return newTask;
}
public Task addNewTaskWithNameAndTypeAndOriginalTaskUri(Task.TaskName name, Task.TaskType type,
Task.OriginalTaskUri originalTaskUri) {
Task newTask = Task.createTaskWithNameAndTypeAndOriginalTaskUri(name, type, originalTaskUri);
this.addNewTaskToList(newTask);
return newTask;
}
private void addNewTaskToList(Task newTask) {
//Here we would also publish a domain event to other entities in the core interested in this event.
//However, we skip this here as it makes the core even more complex (e.g., we have to implement a light-weight
//domain event publisher and subscribers (see "Implementing Domain-Driven Design by V. Vernon, pp. 296ff).
return newTask;
listOfTasks.value.add(newTask);
//This is a simple debug message to see that the task list is growing with each new request
System.out.println("Number of tasks: " + listOfTasks.value.size());
}
public Optional<Task> retrieveTaskById(Task.TaskId id) {
@@ -55,6 +67,43 @@ public class TaskList {
return Optional.empty();
}
public Task changeTaskStatusToAssigned(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider)
throws TaskNotFoundException {
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.ASSIGNED), serviceProvider, Optional.empty());
}
public Task changeTaskStatusToRunning(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider)
throws TaskNotFoundException {
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.RUNNING), serviceProvider, Optional.empty());
}
public Task changeTaskStatusToExecuted(Task.TaskId id, Optional<Task.ServiceProvider> serviceProvider,
Optional<Task.OutputData> outputData) throws TaskNotFoundException {
return changeTaskStatus(id, new Task.TaskStatus(Task.Status.EXECUTED), serviceProvider, outputData);
}
private Task changeTaskStatus(Task.TaskId id, Task.TaskStatus status, Optional<Task.ServiceProvider> serviceProvider,
Optional<Task.OutputData> outputData) {
Optional<Task> taskOpt = retrieveTaskById(id);
if (taskOpt.isEmpty()) {
throw new TaskNotFoundException();
}
Task task = taskOpt.get();
task.setTaskStatus(status);
if (serviceProvider.isPresent()) {
task.setProvider(serviceProvider.get());
}
if (outputData.isPresent()) {
task.setOutputData(outputData.get());
}
return task;
}
@Value
public static class TaskListName {
private String value;
@@ -64,5 +113,4 @@ public class TaskList {
public static class ListOfTasks {
private List<Task> value;
}
}

View File

@@ -0,0 +1,3 @@
package ch.unisg.tapastasks.tasks.domain;
public class TaskNotFoundException extends RuntimeException { }

View File

@@ -1 +1,2 @@
server.port=8081
baseuri=https://tapas-tasks.86-119-34-23.nip.io/