commit 5b835bc6bc8c85605d79cbe2ae6c469e99e2c61f Author: Andrei Ciortea Date: Mon Oct 4 09:13:00 2021 +0200 Initial commit diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml new file mode 100644 index 0000000..619804e --- /dev/null +++ b/.github/workflows/build-and-deploy.yml @@ -0,0 +1,81 @@ +# This workflow will build a package using Maven and then publish it to GitHub packages when a release is created +# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#apache-maven-with-a-settings-path + +name: Build and Deploy + +on: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + java-version: '11' + distribution: 'adopt' + + + - run: mkdir ./target + + - name: Build with Maven + run: mvn -f tapas-tasks/pom.xml --batch-mode --update-snapshots verify + - run: cp ./tapas-tasks/target/tapas-tasks-0.0.1-SNAPSHOT.jar ./target + + - name: Build with Maven + run: mvn -f app/pom.xml --batch-mode --update-snapshots verify + - run: cp ./app/target/app-0.1.0.jar ./target + + - run: cp docker-compose.yml ./target + - name: Archive artifacts + uses: actions/upload-artifact@v1 + with: + name: app + path: ./target/ + + + deploy: + runs-on: ubuntu-latest + needs: [build, ] + steps: + - name: Download app artifacts + uses: actions/download-artifact@v1 + with: + name: app + - name: Copy host via scp + uses: appleboy/scp-action@master + env: + HOST: ${{ secrets.SSH_HOST }} + USERNAME: ${{ secrets.SSH_USER }} + PORT: 22 + KEY: ${{ secrets.SSH_PRIVATE_KEY }} + with: + source: "app/*" + target: "/home/${{ secrets.SSH_USER }}/" + strip_components: 1 + rm: false + overwrite: true + + - name: Executing remote command + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + USERNAME: ${{ secrets.SSH_USER }} + PORT: 22 + KEY: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + cd /home/${{ secrets.SSH_USER }}/ + touch acme.json + sudo chmod 0600 acme.json + sudo echo "PUB_IP=$(wget -qO- http://ipecho.net/plain | xargs echo)" | sed -e 's/\./-/g' > .env + sudo docker-compose up -d + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcc7355 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# TAPAS +This is the main GitHub project for your implementation of the TAPAS application. + +## Project Structure +This project is structured as follows: +* [tapas-tasks](tapas-tasks): standalone project for the Tapas-Tasks micro-service (Spring Boot project) + * [tapas-tasks/src](tapas-tasks/src): source code of the project (following the Hexagonal Architecture) + * [tapas-tasks/pom.xml](tapas-tasks\pom.xml): Maven pom-file +* [app](app): folder as placeholder for a second micro-service (Spring Boot project) +* [docker-compose.yml](docker-compose.yml): Docker Compose configuration file for all services +* [.github/workflows/build-and-deploy.yml](.github/workflows/build-and-deploy.yml): GitHub actions script (CI/CD workflow) + +## How to Add a New Service with Spring Boot + +### Create a new Spring Boot project + +* Recommended: use [Spring Initialzr](https://start.spring.io/) (Maven, Spring Boot 2.5.5, Jar, Java 11, dependencies as needed) +* Set the Spring application properties for your service (e.g., port of the web server) in `src/resources/application.properties` + +### Update the Docker Compose file +Your TAPAS application is a multi-container Docker application ran with [Docker Compose](https://docs.docker.com/compose/). +To add your newly created service to the Docker Compose configuration file, you need to create a new service +definition in [docker-compose.yml](docker-compose.yml): +* copy and edit the `tapas-tasks` service definition from [lines 29-42](https://github.com/scs-asse/tapas/blob/424a5f5aa2d6524acfe95d93000571884ed9d66f/docker-compose.yml#L29-L42) +* change `command` (see [line 31](https://github.com/scs-asse/tapas/blob/main/docker-compose.yml#L31)) +to use the name of the JAR file generated by Maven for your service + * note: if you change the version of your service, you need to update this line to reflect the change +* update the Traefik label names to reflect the name of your new service (see [lines 37-42](https://github.com/scs-asse/tapas/blob/424a5f5aa2d6524acfe95d93000571884ed9d66f/docker-compose.yml#L37-L42)) + * e.g., change `traefik.http.routers.tapas-tasks.rule` to `traefik.http.routers..rule` +* update the Traefik `rule` (see [line 37](https://github.com/scs-asse/tapas/blob/424a5f5aa2d6524acfe95d93000571884ed9d66f/docker-compose.yml#L37)) with the name of your new service: ``Host(`.${PUB_IP}.nip.io`)`` +* update the Traefik `port` (see [line 39](https://github.com/scs-asse/tapas/blob/424a5f5aa2d6524acfe95d93000571884ed9d66f/docker-compose.yml#L39)) with the port configured for your new service + +### Update the GitHub Actions Workflow +This project uses GitHub Actions to build and deploy your TAPAS application whenever a new commit is +pushed on the `main` branch. You can add your new service to the GitHub Actions workflow defined in +[.github/workflows/build-and-deploy.yml](.github/workflows/build-and-deploy.yml): +* copy and edit the definition for `tapas-tasks` from [line 28-30](https://github.com/scs-asse/tapas/blob/424a5f5aa2d6524acfe95d93000571884ed9d66f/.github/workflows/build-and-deploy.yml#L28-L30) +* update the `mvn` command used to build your service to point to the `pom.xml` file of your new service (see [line 29](https://github.com/scs-asse/tapas/blob/424a5f5aa2d6524acfe95d93000571884ed9d66f/.github/workflows/build-and-deploy.yml#L29)) +* update the `cp` command to point to the JAR file of your new service directive (see [line 30](https://github.com/scs-asse/tapas/blob/424a5f5aa2d6524acfe95d93000571884ed9d66f/.github/workflows/build-and-deploy.yml#L30)) + * note you will need to update the complete file path (folder structure and JAR name) + +### How to Run Your Service Locally +You can run and test your micro-service on your local machine just like a regular Maven project: +* Run from IntelliJ: + * Reload *pom.xml* if necessary + * Run the micro-service's main class from IntelliJ for all required projects +* Use Maven to run from the command line: +```shell +mvn spring-boot:run +``` + +## How to Deploy on your VM +1. Start your Ubuntu VM on Switch. + * VM shuts down automatically at 2 AM + * Group admins can do this via https://engines.switch.ch/horizon +2. Push new code to the *main* branch + * Check the status of the workflow on the *Actions* page of the GitHub project + * We recommend to test your project locally before pushing the code to GitHub. The GitHub Organizations + used in the course are on a free tier plan, which comes with [various limits](https://github.com/pricing). +3. Open in your browser `https://app..nip.io` + +For the server IP address (see below), you should use dashes instead of dots, e.g.: `127.0.0.1` becomes `127-0-0-1`. + +## VM Configurations + +Specs (we can upgrade if needed): +* 1 CPU +* 2 GB RAM +* 20 GB HD +* Ubuntu 20.04 + +| Name | Server IP | +|-------|-----------| +|SCS-ASSE-VM-Group1|86.119.35.40| +|SCS-ASSE-VM-Group2|86.119.35.213| +|SCS-ASSE-VM-Group3|86.119.34.242| +|SCS-ASSE-VM-Group4|86.119.35.199| +|SCS-ASSE-VM-Group5|86.119.35.72| + +## Architecture Decision Records +We recommend you to use [adr-tools](https://github.com/npryce/adr-tools) to manage your ADRs here in +this GitHub project in a dedicated folder. The tool works best on a Mac OS or Linux machine. \ No newline at end of file diff --git a/app/pom.xml b/app/pom.xml new file mode 100644 index 0000000..5f4a1fe --- /dev/null +++ b/app/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + + com.dockerforjavadevelopers + app + 0.1.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + + + org.springframework.boot + spring-boot-starter-web + + + + junit + junit + 4.13.1 + test + + + + + + com.dockerforjavadevelopers.hello.Application + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/app/src/main/java/com/app/hello/Application.java b/app/src/main/java/com/app/hello/Application.java new file mode 100755 index 0000000..8f330a7 --- /dev/null +++ b/app/src/main/java/com/app/hello/Application.java @@ -0,0 +1,19 @@ +package com.dockerforjavadevelopers.hello; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + + public static void main(String[] args) { + ApplicationContext ctx = SpringApplication.run(Application.class, args); + + } + +} diff --git a/app/src/main/java/com/app/hello/HelloController.java b/app/src/main/java/com/app/hello/HelloController.java new file mode 100755 index 0000000..8d69328 --- /dev/null +++ b/app/src/main/java/com/app/hello/HelloController.java @@ -0,0 +1,14 @@ +package com.dockerforjavadevelopers.hello; + +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; + +@RestController +public class HelloController { + + @RequestMapping("/") + public String index() { + return "Hello World! Nice to see you :-)\n"; + } + +} diff --git a/app/src/test/java/com/app/hello/DummyTest.java b/app/src/test/java/com/app/hello/DummyTest.java new file mode 100755 index 0000000..7d25f73 --- /dev/null +++ b/app/src/test/java/com/app/hello/DummyTest.java @@ -0,0 +1,13 @@ +package com.dockerforjavadevelopers.hello; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class DummyTest { + + @Test + public void aTest() { + assertEquals(true, true); + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6d8f256 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: "3.0" + +services: + reverse-proxy: + image: traefik:v2.1.3 + command: + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --providers.docker=true + - --certificatesResolvers.le.acme.httpChallenge.entryPoint=web + - --certificatesresolvers.le.acme.email=martin.eigenmann@unisg.ch + - --certificatesresolvers.le.acme.storage=/acme.json + - --providers.docker.exposedByDefault=false + - --serversTransport.insecureSkipVerify=true + ports: + - "80:80" + - "443:443" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./acme.json:/acme.json + restart: unless-stopped + labels: + - "traefik.enable=true" + - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" + - "traefik.http.routers.http-catchall.entrypoints=web" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + + tapas-tasks: + image: openjdk + command: "java -jar /data/tapas-tasks-0.0.1-SNAPSHOT.jar" + restart: unless-stopped + volumes: + - ./:/data/ + labels: + - "traefik.enable=true" + - "traefik.http.routers.tapas-tasks.rule=Host(`tapas-tasks.${PUB_IP}.nip.io`)" + - "traefik.http.routers.tapas-tasks.service=tapas-tasks" + - "traefik.http.services.tapas-tasks.loadbalancer.server.port=8081" + - "traefik.http.routers.tapas-tasks.tls=true" + - "traefik.http.routers.tapas-tasks.entryPoints=web,websecure" + - "traefik.http.routers.tapas-tasks.tls.certresolver=le" + + app: + image: openjdk + command: "java -jar /data/app-0.1.0.jar" + restart: unless-stopped + volumes: + - ./:/data/ + labels: + - "traefik.enable=true" + - "traefik.http.routers.app.rule=Host(`app.${PUB_IP}.nip.io`)" + - "traefik.http.routers.app.service=app" + - "traefik.http.services.app.loadbalancer.server.port=8080" + - "traefik.http.routers.app.tls=true" + - "traefik.http.routers.app.entryPoints=web,websecure" + - "traefik.http.routers.app.tls.certresolver=le" diff --git a/tapas-tasks/.editorconfig b/tapas-tasks/.editorconfig new file mode 100644 index 0000000..c4f3e5b --- /dev/null +++ b/tapas-tasks/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true diff --git a/tapas-tasks/.gitignore b/tapas-tasks/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/tapas-tasks/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/tapas-tasks/.mvn/wrapper/MavenWrapperDownloader.java b/tapas-tasks/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..e76d1f3 --- /dev/null +++ b/tapas-tasks/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/tapas-tasks/.mvn/wrapper/maven-wrapper.jar b/tapas-tasks/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/tapas-tasks/.mvn/wrapper/maven-wrapper.jar differ diff --git a/tapas-tasks/.mvn/wrapper/maven-wrapper.properties b/tapas-tasks/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..ffdc10e --- /dev/null +++ b/tapas-tasks/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/tapas-tasks/README.md b/tapas-tasks/README.md new file mode 100644 index 0000000..f083776 --- /dev/null +++ b/tapas-tasks/README.md @@ -0,0 +1,71 @@ +# tapas-tasks + +Micro-service for Managing Tasks in a Task List implemented following Hexagonal Architecture. + +Based on examples from book "Get Your Hands Dirty on Clean Architecture" by Tom Hombergs + +Technologies: Java, Spring Boot, Maven + +**Note:** this repository contains an [EditorConfig](https://editorconfig.org/) file (`.editorconfig`) +with default editor settings. EditorConfig is supported out-of-the-box by the IntelliJ IDE. To help maintain +consistent code styles, we recommend to reuse this editor configuration file in all your services. + +## 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. + +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: +* `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" +}' + +HTTP/1.1 201 +Content-Type: application/json +Content-Length: 142 +Date: Sun, 03 Oct 2021 17:25:32 GMT + +{ + "taskType" : "type1", + "taskState" : "OPEN", + "taskListName" : "tapas-tasks-tutors", + "taskName" : "task1", + "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" +} +``` + +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`). + +### Retrieving a task + +The representation of a task is retrieved via an `HTTP GET` request to the `/tasks/` endpoint. + +A sample HTTP request with `curl`: +```shell +curl -i --location --request GET 'http://localhost:8081/tasks/53cb19d6-2d9b-486f-98c7-c96c93b037f0' + +HTTP/1.1 200 +Content-Type: application/json +Content-Length: 142 +Date: Sun, 03 Oct 2021 17:27:06 GMT + +{ + "taskType" : "type1", + "taskState" : "OPEN", + "taskListName" : "tapas-tasks-tutors", + "taskName" : "task1", + "taskId" : "53cb19d6-2d9b-486f-98c7-c96c93b037f0" +} +``` diff --git a/tapas-tasks/mvnw b/tapas-tasks/mvnw new file mode 100755 index 0000000..a16b543 --- /dev/null +++ b/tapas-tasks/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/tapas-tasks/mvnw.cmd b/tapas-tasks/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/tapas-tasks/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/tapas-tasks/pom.xml b/tapas-tasks/pom.xml new file mode 100644 index 0000000..a5b6587 --- /dev/null +++ b/tapas-tasks/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.3 + + + ch.unisg + tapas-tasks + 0.0.1-SNAPSHOT + tapas-tasks + TAPAS Tasks + + 11 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-starter-validation + + + + javax.transaction + javax.transaction-api + 1.2 + + + javax.validation + validation-api + 1.1.0.Final + + + + org.json + json + 20210307 + + + com.github.java-json-tools + json-patch + 1.13 + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java new file mode 100644 index 0000000..40fa5da --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/TapasTasksApplication.java @@ -0,0 +1,17 @@ +package ch.unisg.tapastasks; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.util.Collections; + +@SpringBootApplication +public class TapasTasksApplication { + + public static void main(String[] args) { + + SpringApplication tapasTasksApp = new SpringApplication(TapasTasksApplication.class); + tapasTasksApp.run(args); + } + +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/common/SelfValidating.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/common/SelfValidating.java new file mode 100644 index 0000000..d7cf6fc --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/common/SelfValidating.java @@ -0,0 +1,30 @@ +package ch.unisg.tapastasks.common; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; + +public abstract class SelfValidating { + + private Validator validator; + + public SelfValidating() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + /** + * Evaluates all Bean Validations on the attributes of this + * instance. + */ + protected void validateSelf() { + Set> violations = validator.validate((T) this); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } +} + diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java new file mode 100644 index 0000000..53bebc1 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/AddNewTaskToTaskListWebController.java @@ -0,0 +1,42 @@ +package ch.unisg.tapastasks.tasks.adapter.in.web; + +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 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 javax.validation.ConstraintViolationException; + +@RestController +public class AddNewTaskToTaskListWebController { + private final AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase; + + public AddNewTaskToTaskListWebController(AddNewTaskToTaskListUseCase addNewTaskToTaskListUseCase) { + this.addNewTaskToTaskListUseCase = addNewTaskToTaskListUseCase; + } + + @PostMapping(path = "/tasks/", consumes = {TaskMediaType.TASK_MEDIA_TYPE}) + public ResponseEntity addNewTaskTaskToTaskList(@RequestBody Task task) { + try { + AddNewTaskToTaskListCommand command = new AddNewTaskToTaskListCommand( + task.getTaskName(), task.getTaskType() + ); + + Task newTask = addNewTaskToTaskListUseCase.addNewTaskToTaskList(command); + + // Add the content type as a response header + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, TaskMediaType.TASK_MEDIA_TYPE); + + return new ResponseEntity<>(TaskMediaType.serialize(newTask), responseHeaders, HttpStatus.CREATED); + } catch (ConstraintViolationException e) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage()); + } + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java new file mode 100644 index 0000000..0035986 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/RetrieveTaskFromTaskListWebController.java @@ -0,0 +1,40 @@ +package ch.unisg.tapastasks.tasks.adapter.in.web; + +import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListCommand; +import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase; +import ch.unisg.tapastasks.tasks.domain.Task; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Optional; + +@RestController +public class RetrieveTaskFromTaskListWebController { + private final RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase; + + public RetrieveTaskFromTaskListWebController(RetrieveTaskFromTaskListUseCase retrieveTaskFromTaskListUseCase) { + this.retrieveTaskFromTaskListUseCase = retrieveTaskFromTaskListUseCase; + } + + @GetMapping(path = "/tasks/{taskId}") + public ResponseEntity retrieveTaskFromTaskList(@PathVariable("taskId") String taskId) { + RetrieveTaskFromTaskListCommand command = new RetrieveTaskFromTaskListCommand(new Task.TaskId(taskId)); + Optional updatedTaskOpt = retrieveTaskFromTaskListUseCase.retrieveTaskFromTaskList(command); + + // Check if the task with the given identifier exists + if (updatedTaskOpt.isEmpty()) { + // If not, through a 404 Not Found status code + 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); + + return new ResponseEntity<>(TaskMediaType.serialize(updatedTaskOpt.get()), responseHeaders, + HttpStatus.OK); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java new file mode 100644 index 0000000..3c555e5 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/in/web/TaskMediaType.java @@ -0,0 +1,23 @@ +package ch.unisg.tapastasks.tasks.adapter.in.web; + +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import org.json.JSONObject; + +final public class TaskMediaType { + public static final String TASK_MEDIA_TYPE = "application/json"; + + public static String serialize(Task task) { + JSONObject payload = new JSONObject(); + + payload.put("taskId", task.getTaskId().getValue()); + payload.put("taskName", task.getTaskName().getValue()); + payload.put("taskType", task.getTaskType().getValue()); + payload.put("taskState", task.getTaskState().getValue()); + payload.put("taskListName", TaskList.getTapasTaskList().getTaskListName().getValue()); + + return payload.toString(); + } + + private TaskMediaType() { } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java new file mode 100644 index 0000000..db02f2a --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/adapter/out/web/PublishNewTaskAddedEventWebAdapter.java @@ -0,0 +1,58 @@ +package ch.unisg.tapastasks.tasks.adapter.out.web; + +import ch.unisg.tapastasks.tasks.application.port.out.NewTaskAddedEventPort; +import ch.unisg.tapastasks.tasks.domain.NewTaskAddedEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; + +@Component +@Primary +public class PublishNewTaskAddedEventWebAdapter implements NewTaskAddedEventPort { + + //This is the base URI of the service interested in this event (in my setup, running locally as separate Spring Boot application) + String server = "http://127.0.0.1:8082"; + + @Override + public void publishNewTaskAddedEvent(NewTaskAddedEvent event) { + + //Here we would need to work with DTOs in case the payload of calls becomes more complex + + var values = new HashMap() {{ + put("taskname",event.taskName); + put("tasklist",event.taskListName); + }}; + + var objectMapper = new ObjectMapper(); + String requestBody = null; + try { + requestBody = objectMapper.writeValueAsString(values); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(server+"/roster/newtask/")) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + /** Needs the other service running + try { + HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + **/ + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java new file mode 100644 index 0000000..a0e0fec --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListCommand.java @@ -0,0 +1,23 @@ +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 lombok.Value; + +import javax.validation.constraints.NotNull; + +@Value +public class AddNewTaskToTaskListCommand extends SelfValidating { + @NotNull + private final TaskName taskName; + + @NotNull + private final TaskType taskType; + + public AddNewTaskToTaskListCommand(TaskName taskName, TaskType taskType) { + this.taskName = taskName; + this.taskType = taskType; + this.validateSelf(); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListUseCase.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListUseCase.java new file mode 100644 index 0000000..74f1c60 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/AddNewTaskToTaskListUseCase.java @@ -0,0 +1,7 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +public interface AddNewTaskToTaskListUseCase { + Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListCommand.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListCommand.java new file mode 100644 index 0000000..0796cb0 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListCommand.java @@ -0,0 +1,18 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.common.SelfValidating; +import ch.unisg.tapastasks.tasks.domain.Task.TaskId; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +@Value +public class RetrieveTaskFromTaskListCommand extends SelfValidating { + @NotNull + private final TaskId taskId; + + public RetrieveTaskFromTaskListCommand(TaskId taskId) { + this.taskId = taskId; + this.validateSelf(); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java new file mode 100644 index 0000000..74479f3 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/in/RetrieveTaskFromTaskListUseCase.java @@ -0,0 +1,9 @@ +package ch.unisg.tapastasks.tasks.application.port.in; + +import ch.unisg.tapastasks.tasks.domain.Task; + +import java.util.Optional; + +public interface RetrieveTaskFromTaskListUseCase { + Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListCommand command); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/NewTaskAddedEventPort.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/NewTaskAddedEventPort.java new file mode 100644 index 0000000..95bf4d8 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/port/out/NewTaskAddedEventPort.java @@ -0,0 +1,8 @@ +package ch.unisg.tapastasks.tasks.application.port.out; + +import ch.unisg.tapastasks.tasks.domain.NewTaskAddedEvent; + +public interface NewTaskAddedEventPort { + + void publishNewTaskAddedEvent(NewTaskAddedEvent event); +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java new file mode 100644 index 0000000..48c75a6 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/AddNewTaskToTaskListService.java @@ -0,0 +1,39 @@ +package ch.unisg.tapastasks.tasks.application.service; + +import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListCommand; +import ch.unisg.tapastasks.tasks.application.port.in.AddNewTaskToTaskListUseCase; +import ch.unisg.tapastasks.tasks.application.port.out.NewTaskAddedEventPort; +import ch.unisg.tapastasks.tasks.domain.Task; + +import ch.unisg.tapastasks.tasks.domain.NewTaskAddedEvent; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import javax.transaction.Transactional; + +@RequiredArgsConstructor +@Component +@Transactional +public class AddNewTaskToTaskListService implements AddNewTaskToTaskListUseCase { + + private final NewTaskAddedEventPort newTaskAddedEventPort; + + @Override + public Task addNewTaskToTaskList(AddNewTaskToTaskListCommand command) { + TaskList taskList = TaskList.getTapasTaskList(); + Task newTask = 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. + //Domain events are usually rather "fat". In our implementation we simplify at this point. In general, it is + //not recommended to emit a domain event via an application service! You should first emit the domain event in + //the core and then the integration event in the application layer. + if (newTask != null) { + NewTaskAddedEvent newTaskAdded = new NewTaskAddedEvent(newTask.getTaskName().getValue(), + taskList.getTaskListName().getValue()); + newTaskAddedEventPort.publishNewTaskAddedEvent(newTaskAdded); + } + + return newTask; + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java new file mode 100644 index 0000000..b256273 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/application/service/RetrieveTaskFromTaskListService.java @@ -0,0 +1,22 @@ +package ch.unisg.tapastasks.tasks.application.service; + +import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListCommand; +import ch.unisg.tapastasks.tasks.application.port.in.RetrieveTaskFromTaskListUseCase; +import ch.unisg.tapastasks.tasks.domain.Task; +import ch.unisg.tapastasks.tasks.domain.TaskList; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import javax.transaction.Transactional; +import java.util.Optional; + +@RequiredArgsConstructor +@Component +@Transactional +public class RetrieveTaskFromTaskListService implements RetrieveTaskFromTaskListUseCase { + @Override + public Optional retrieveTaskFromTaskList(RetrieveTaskFromTaskListCommand command) { + TaskList taskList = TaskList.getTapasTaskList(); + return taskList.retrieveTaskById(command.getTaskId()); + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/NewTaskAddedEvent.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/NewTaskAddedEvent.java new file mode 100644 index 0000000..32f5966 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/NewTaskAddedEvent.java @@ -0,0 +1,12 @@ +package ch.unisg.tapastasks.tasks.domain; + +/**This is a domain event (these are usually much fatter)**/ +public class NewTaskAddedEvent { + public String taskName; + public String taskListName; + + public NewTaskAddedEvent(String taskName, String taskListName) { + this.taskName = taskName; + this.taskListName = taskListName; + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java new file mode 100644 index 0000000..0dcafc3 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/Task.java @@ -0,0 +1,59 @@ +package ch.unisg.tapastasks.tasks.domain; + +import lombok.Getter; +import lombok.Setter; +import lombok.Value; + +import java.util.UUID; + +/**This is a domain entity**/ +public class Task { + public enum State { + OPEN, ASSIGNED, RUNNING, EXECUTED + } + + @Getter + private final TaskId taskId; + + @Getter + private final TaskName taskName; + + @Getter + private final TaskType taskType; + + @Getter + private TaskState taskState; + + 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()); + } + + 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); + } + + @Value + public static class TaskId { + private String value; + } + + @Value + public static class TaskName { + private String value; + } + + @Value + public static class TaskState { + private State value; + } + + @Value + public static class TaskType { + private String value; + } +} diff --git a/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java new file mode 100644 index 0000000..2b90da5 --- /dev/null +++ b/tapas-tasks/src/main/java/ch/unisg/tapastasks/tasks/domain/TaskList.java @@ -0,0 +1,68 @@ +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; + + +/**This is our aggregate root**/ +public class TaskList { + + @Getter + private final TaskListName taskListName; + + @Getter + private final ListOfTasks listOfTasks; + + //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") + private static final TaskList taskList = new TaskList(new TaskListName("tapas-tasks-tutors")); + + private TaskList(TaskListName taskListName) { + this.taskListName = taskListName; + this.listOfTasks = new ListOfTasks(new LinkedList()); + } + + public static TaskList getTapasTaskList() { + return 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()); + //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; + } + + public Optional retrieveTaskById(Task.TaskId id) { + for (Task task : listOfTasks.value) { + if (task.getTaskId().getValue().equalsIgnoreCase(id.getValue())) { + return Optional.of(task); + } + } + + return Optional.empty(); + } + + @Value + public static class TaskListName { + private String value; + } + + @Value + public static class ListOfTasks { + private List value; + } + +} diff --git a/tapas-tasks/src/main/resources/application.properties b/tapas-tasks/src/main/resources/application.properties new file mode 100644 index 0000000..4d360de --- /dev/null +++ b/tapas-tasks/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=8081 diff --git a/tapas-tasks/src/test/java/ch/unisg/tapastasks/TapasTasksApplicationTests.java b/tapas-tasks/src/test/java/ch/unisg/tapastasks/TapasTasksApplicationTests.java new file mode 100644 index 0000000..b96cb8b --- /dev/null +++ b/tapas-tasks/src/test/java/ch/unisg/tapastasks/TapasTasksApplicationTests.java @@ -0,0 +1,13 @@ +package ch.unisg.tapastasks; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TapasTasksApplicationTests { + + @Test + void contextLoads() { + } + +}