Skip to content

Commit

Permalink
Switch to Spring Boot
Browse files Browse the repository at this point in the history
  • Loading branch information
SvenWoltmann committed Nov 7, 2023
1 parent c3dde7c commit 66fa799
Show file tree
Hide file tree
Showing 43 changed files with 383 additions and 320 deletions.
21 changes: 9 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,26 @@ The following diagram shows the hexagonal architecture of the application along

![Hexagonal Architecture Modules](doc/hexagonal-architecture-modules.png)

The `model` module is not represented as a hexagon because it is not defined by the Hexagonal Architecture. Hexagonal Architecture leaves open what happens inside the application hexagon.
The `model` module is not represented as a hexagon because it is not defined by the Hexagonal Architecture. Hexagonal Architecture leaves open what happens inside the application hexagon.

# How to Run the Application

You can run the application in Quarkus dev mode with the following command:
The easiest way to run the application is to start the `main` method of the `Launcher` class (you'll find it in the `boostrap` module) from your IDE.

```shell
mvn test-compile quarkus:dev
```
By default, the application will run with the in-memory persistence option.

You can use one of the following VM options to select a persistence mechanism:
To select the MySQL persistence option, start it with the following VM option:

* `-Dpersistence=inmemory` to select the in-memory persistence option (default)
* `-Dpersistence=mysql` to select the MySQL option
`-Dspring.profiles.active=mysql`

For example, to run the application in MySQL mode, enter:
If you selected the MySQL option, you will need a running MySQL database. The easiest way to start one is to use the following Docker command:

```shell
mvn test-compile quarkus:dev -Dpersistence=mysql
docker run --name hexagon-mysql -d -p3306:3306 \
-e MYSQL_DATABASE=shop -e MYSQL_ROOT_PASSWORD=test mysql:8.1
```

In dev mode, Quarkus will automatically start a MySQL database using Docker,
and it will automatically create all database tables.
The connection parameters for the database are hardcoded in `application-mysql.properties`. If you are using the Docker container as described above, you can leave the connection parameters as they are. Otherwise, you may need to adjust them.

# Example Curl Commands

Expand Down
38 changes: 18 additions & 20 deletions adapter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,44 +23,42 @@

<!-- External -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-mysql</artifactId>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Test scope -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,34 @@
import eu.happycoders.shop.application.service.cart.EmptyCartService;
import eu.happycoders.shop.application.service.cart.GetCartService;
import eu.happycoders.shop.application.service.product.FindProductsService;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

class QuarkusAppConfig {
@SpringBootApplication
public class SpringAppConfig {

@Inject Instance<CartRepository> cartRepository;
@Autowired CartRepository cartRepository;

@Inject Instance<ProductRepository> productRepository;
@Autowired ProductRepository productRepository;

@Produces
@ApplicationScoped
@Bean
GetCartUseCase getCartUseCase() {
return new GetCartService(cartRepository.get());
return new GetCartService(cartRepository);
}

@Produces
@ApplicationScoped
@Bean
EmptyCartUseCase emptyCartUseCase() {
return new EmptyCartService(cartRepository.get());
return new EmptyCartService(cartRepository);
}

@Produces
@ApplicationScoped
@Bean
FindProductsUseCase findProductsUseCase() {
return new FindProductsService(productRepository.get());
return new FindProductsService(productRepository);
}

@Produces
@ApplicationScoped
@Bean
AddToCartUseCase addToCartUseCase() {
return new AddToCartService(cartRepository.get(), productRepository.get());
return new AddToCartService(cartRepository, productRepository);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,20 @@
import eu.happycoders.shop.model.cart.NotEnoughItemsInStockException;
import eu.happycoders.shop.model.customer.CustomerId;
import eu.happycoders.shop.model.product.ProductId;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* REST controller for all shopping cart use cases.
*
* @author Sven Woltmann
*/
@Path("/carts")
@Produces(MediaType.APPLICATION_JSON)
@RestController
@RequestMapping(path = "/carts")
public class AddToCartController {

private final AddToCartUseCase addToCartUseCase;
Expand All @@ -33,24 +32,22 @@ public AddToCartController(AddToCartUseCase addToCartUseCase) {
this.addToCartUseCase = addToCartUseCase;
}

@POST
@Path("/{customerId}/line-items")
@PostMapping("/{customerId}/line-items")
public CartWebModel addLineItem(
@PathParam("customerId") String customerIdString,
@QueryParam("productId") String productIdString,
@QueryParam("quantity") int quantity) {
@PathVariable("customerId") String customerIdString,
@RequestParam("productId") String productIdString,
@RequestParam("quantity") int quantity) {
CustomerId customerId = parseCustomerId(customerIdString);
ProductId productId = parseProductId(productIdString);

try {
Cart cart = addToCartUseCase.addToCart(customerId, productId, quantity);
return CartWebModel.fromDomainModel(cart);
} catch (ProductNotFoundException e) {
throw clientErrorException(
Response.Status.BAD_REQUEST, "The requested product does not exist");
throw clientErrorException(HttpStatus.BAD_REQUEST, "The requested product does not exist");
} catch (NotEnoughItemsInStockException e) {
throw clientErrorException(
Response.Status.BAD_REQUEST, "Only %d items in stock".formatted(e.itemsInStock()));
HttpStatus.BAD_REQUEST, "Only %d items in stock".formatted(e.itemsInStock()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,19 @@

import eu.happycoders.shop.application.port.in.cart.EmptyCartUseCase;
import eu.happycoders.shop.model.customer.CustomerId;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* REST controller for all shopping cart use cases.
*
* @author Sven Woltmann
*/
@Path("/carts")
@Produces(MediaType.APPLICATION_JSON)
@RestController
@RequestMapping(path = "/carts")
public class EmptyCartController {

private final EmptyCartUseCase emptyCartUseCase;
Expand All @@ -25,10 +25,10 @@ public EmptyCartController(EmptyCartUseCase emptyCartUseCase) {
this.emptyCartUseCase = emptyCartUseCase;
}

@DELETE
@Path("/{customerId}")
public void deleteCart(@PathParam("customerId") String customerIdString) {
@DeleteMapping("/{customerId}")
public ResponseEntity<Void> deleteCart(@PathVariable("customerId") String customerIdString) {
CustomerId customerId = parseCustomerId(customerIdString);
emptyCartUseCase.emptyCart(customerId);
return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@
import eu.happycoders.shop.application.port.in.cart.GetCartUseCase;
import eu.happycoders.shop.model.cart.Cart;
import eu.happycoders.shop.model.customer.CustomerId;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* REST controller for all shopping cart use cases.
*
* @author Sven Woltmann
*/
@Path("/carts")
@Produces(MediaType.APPLICATION_JSON)
@RestController
@RequestMapping(path = "/carts")
public class GetCartController {

private final GetCartUseCase getCartUseCase;
Expand All @@ -26,9 +25,8 @@ public GetCartController(GetCartUseCase getCartUseCase) {
this.getCartUseCase = getCartUseCase;
}

@GET
@Path("/{customerId}")
public CartWebModel getCart(@PathParam("customerId") String customerIdString) {
@GetMapping("/{customerId}")
public CartWebModel getCart(@PathVariable("customerId") String customerIdString) {
CustomerId customerId = parseCustomerId(customerIdString);
Cart cart = getCartUseCase.getCart(customerId);
return CartWebModel.fromDomainModel(cart);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.happycoders.shop.adapter.in.rest.common;

import lombok.Getter;
import org.springframework.http.ResponseEntity;

/**
* An exception to be thrown in case of a client error (e.g., invalid input).
*
* @author Sven Woltmann
*/
public class ClientErrorException extends RuntimeException {

@Getter private final ResponseEntity<ErrorEntity> response;

public ClientErrorException(ResponseEntity<ErrorEntity> response) {
super(response.getBody().errorMessage());
this.response = response;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.happycoders.shop.adapter.in.rest.common;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
* Handles {@link ClientErrorException} by returning a JSON body containing the error details.
*
* @author Sven Woltmann
*/
@RestControllerAdvice
public class ClientErrorHandler {

@ExceptionHandler(ClientErrorException.class)
public ResponseEntity<ErrorEntity> handleProductNotFoundException(ClientErrorException ex) {
return ex.getResponse();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package eu.happycoders.shop.adapter.in.rest.common;

import jakarta.ws.rs.ClientErrorException;
import jakarta.ws.rs.core.Response;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

/**
* Common functionality for all REST controllers.
Expand All @@ -12,12 +12,12 @@ public final class ControllerCommons {

private ControllerCommons() {}

public static ClientErrorException clientErrorException(Response.Status status, String message) {
public static ClientErrorException clientErrorException(HttpStatus status, String message) {
return new ClientErrorException(errorResponse(status, message));
}

public static Response errorResponse(Response.Status status, String message) {
ErrorEntity errorEntity = new ErrorEntity(status.getStatusCode(), message);
return Response.status(status).entity(errorEntity).build();
public static ResponseEntity<ErrorEntity> errorResponse(HttpStatus status, String message) {
ErrorEntity errorEntity = new ErrorEntity(status.value(), message);
return ResponseEntity.status(status.value()).body(errorEntity);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import static eu.happycoders.shop.adapter.in.rest.common.ControllerCommons.clientErrorException;

import eu.happycoders.shop.model.customer.CustomerId;
import jakarta.ws.rs.core.Response;
import org.springframework.http.HttpStatus;

/**
* A parser for customer IDs, throwing a {@link jakarta.ws.rs.ClientErrorException} for invalid
* customer IDs.
* A parser for customer IDs, throwing a {@link
* org.springframework.web.client.HttpClientErrorException} for invalid customer IDs.
*
* @author Sven Woltmann
*/
Expand All @@ -19,7 +19,7 @@ public static CustomerId parseCustomerId(String string) {
try {
return new CustomerId(Integer.parseInt(string));
} catch (IllegalArgumentException e) {
throw clientErrorException(Response.Status.BAD_REQUEST, "Invalid 'customerId'");
throw clientErrorException(HttpStatus.BAD_REQUEST, "Invalid 'customerId'");
}
}
}
Loading

0 comments on commit 66fa799

Please sign in to comment.