Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# IntelliJ
.idea/
*.iml
**/target/

10 changes: 10 additions & 0 deletions sync_to_async/BIBLIO.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
* Docker best practices
** https://github.com/docker/docker.github.io/blob/master/develop/develop-images/dockerfile_best-practices.md
* Monitoring RabbitMQ with Prom
** https://www.rabbitmq.com/prometheus.html
** https://github.com/rabbitmq/rabbitmq-server/tree/master/deps/rabbitmq_prometheus/docker

* Compose Networking with host network
https://forums.docker.com/t/localhost-and-docker-compose-networking-issue/23100/5
-> host.docker.internal

64 changes: 64 additions & 0 deletions sync_to_async/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
version: "3.9"
volumes:
prometheus:
grafana:
services:
rabbitmq:
# https://hub.docker.com/_/rabbitmq
image: rabbitmq:3.9.7-management-alpine
container_name: 'rabbitmq'
ports:
- 5672:5672
- 15672:15672 # Management
- 15692:15692 # Prometheus

# login : postgres/example
postgresql:
# https://hub.docker.com/_/postgres?tab=description
image: postgres:14.0-alpine
container_name: 'postgres'
ports:
- 5432:5432
environment:
POSTGRES_PASSWORD: example

adminer:
# https://hub.docker.com/_/adminer?tab=tags
image: adminer:4.8.1-standalone
container_name: 'adminer'
ports:
- 8080:8080
restart: always

prometheus:
# https://hub.docker.com/r/prom/prometheus/tags
image: prom/prometheus:v2.30.3
container_name: 'prometheus'
ports:
- 9090:9090
volumes:
- prometheus:/prometheus
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml

# login : guest/guest
grafana:
# https://hub.docker.com/r/grafana/grafana/tags
image: grafana/grafana:8.2.1
container_name: 'grafana'
ports:
- 3000:3000
volumes:
- grafana:/var/lib/grafana
- ./grafana/dashboards.yml:/etc/grafana/provisioning/dashboards/rabbitmq.yaml
- ./grafana/datasources.yml:/etc/grafana/provisioning/datasources/prometheus.yaml
- ./grafana/dashboards:/dashboards
environment:
# https://grafana.com/plugins/flant-statusmap-panel
# https://grafana.com/plugins/grafana-piechart-panel
GF_INSTALL_PLUGINS: "flant-statusmap-panel,grafana-piechart-panel"

front:
build : ./front
container_name: 'front'
ports:
- 8082:8082
9 changes: 9 additions & 0 deletions sync_to_async/front/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
FROM maven:3.8.3-openjdk-17 AS build
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml clean package

FROM eclipse-temurin:17-jdk-focal
COPY --from=build /home/app/target/front-0.0.1-SNAPSHOT.jar /usr/local/lib/app.jar
EXPOSE 8001
ENTRYPOINT ["java","-jar","/usr/local/lib/app.jar"]
68 changes: 68 additions & 0 deletions sync_to_async/front/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.github.lernejo</groupId>
<artifactId>front</artifactId>
<version>0.0.1-SNAPSHOT</version>

<properties>
<java.version>17</java.version>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>

<spring-boot.version>2.5.5</spring-boot.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.github.lernejo.front;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FrontApplication {
public static void main(String[] args) {
SpringApplication.run(FrontApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.github.lernejo.front.basket;

import com.github.lernejo.front.user.UserNotConnectedException;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/basket")
record BasketController(BasketService basketService) {

@PostMapping("/add")
BasketInfo add(@RequestParam int productId, @RequestParam int quantity) throws ProductUnavailableException, UserNotConnectedException {
return basketService.add(productId, quantity);
}

@GetMapping
BasketInfo getBasketInfo() throws UserNotConnectedException {
return basketService.getBasketInfo();
}

@GetMapping("/content")
List<BasketProduct> getBasketContent() throws UserNotConnectedException {
return basketService.getBasketContent();
}

@PostMapping("/payment")
PaymentResult proceedToPayment(@RequestBody PaymentInformation info) throws RemoteServiceUnavailableException, UserNotConnectedException {
return basketService.proceedToPayment(info);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.github.lernejo.front.basket;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class BasketHandlerExceptionResolver implements HandlerExceptionResolver {

@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.github.lernejo.front.basket;

import java.util.List;

record BasketInfo(int size, double price) {
public static BasketInfo from(List<BasketProduct> basket) {
return new BasketInfo(
basket.stream().mapToInt(p -> p.quantity()).sum(),
basket.stream().mapToDouble(p -> p.unitPrice() * p.quantity()).sum()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.github.lernejo.front.basket;

record BasketProduct(String name, double unitPrice, int quantity) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.github.lernejo.front.basket;

import com.github.lernejo.front.user.SessionContext;
import com.github.lernejo.front.user.UserNotConnectedException;
import com.github.lernejo.front.user.UserSession;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;

@Repository
class BasketRepository {

private static final String BASKET_SESSION_KEY = "basket";

BasketInfo add(BasketProduct basketProduct) throws UserNotConnectedException {
List<BasketProduct> basket = getBasket();
basket.add(basketProduct);
return BasketInfo.from(basket);
}

public List<BasketProduct> getCurrent() throws UserNotConnectedException {
return getBasket();
}

private List<BasketProduct> getBasket() throws UserNotConnectedException {
UserSession userSession = SessionContext.getOrThrow();
List<BasketProduct> basket = userSession.get(BASKET_SESSION_KEY);
if (basket == null) {
basket = new ArrayList<>();
userSession.put(BASKET_SESSION_KEY, basket);
}
return basket;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.github.lernejo.front.basket;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.lernejo.front.product.ProductService;
import com.github.lernejo.front.product.StockRemovalStatus;
import com.github.lernejo.front.user.UserNotConnectedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.YearMonth;
import java.util.List;

@Service
final class BasketService {
private final Logger logger = LoggerFactory.getLogger(BasketService.class);
private final HttpClient httpClient = HttpClient.newHttpClient();

private final ProductService productService;
private final BasketRepository basketRepository;
private final String paymentServiceUrl;
private final ObjectMapper objectMapper;

BasketService(ProductService productService,
BasketRepository basketRepository,
@Value("${service.payment.url}") String paymentServiceUrl,
ObjectMapper objectMapper) {
this.productService = productService;
this.basketRepository = basketRepository;
this.paymentServiceUrl = paymentServiceUrl;
this.objectMapper = objectMapper;
}

BasketInfo add(int productId, int quantity) throws ProductUnavailableException, UserNotConnectedException {
StockRemovalStatus stockRemovalStatus = productService.removeFromStock(productId, quantity);
if (stockRemovalStatus.status() == StockRemovalStatus.Status.REMOVED) {
return basketRepository.add(new BasketProduct(stockRemovalStatus.name(), stockRemovalStatus.price(), stockRemovalStatus.quantity()));
} else {
throw new ProductUnavailableException(stockRemovalStatus.name(), stockRemovalStatus.quantity());
}
}

BasketInfo getBasketInfo() throws UserNotConnectedException {
List<BasketProduct> current = getBasketContent();
return BasketInfo.from(current);
}

List<BasketProduct> getBasketContent() throws UserNotConnectedException {
return basketRepository.getCurrent();
}

public PaymentResult proceedToPayment(PaymentInformation info) throws RemoteServiceUnavailableException, UserNotConnectedException {
var payload = new PaymentServiceRequest(
info.firstname() + " " + info.lastname(),
info.cardNumber(),
info.expirationDate(),
info.cryptoCode(),
getBasketInfo().price()
);
HttpRequest paymentRequest = buildRequest(payload);
try {
HttpResponse<String> response = httpClient.send(paymentRequest, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
return deserialize(response.body(), PaymentResult.class);
} catch (IOException | InterruptedException e) {
logger.error("Failed to proceed to payment, service unavailable", e);
throw new RemoteServiceUnavailableException();
}
}

private HttpRequest buildRequest(PaymentServiceRequest payload) {
return HttpRequest.newBuilder()
.uri(URI.create(paymentServiceUrl + "/api/payment"))
.setHeader("Accept", "application/json")
.setHeader("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(serialize(payload)))
.build();
}

private String serialize(Object obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new UncheckedIOException(e);
}
}

private <T> T deserialize(String json, Class<T> clazz) {
try {
return objectMapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new UncheckedIOException(e);
}
}

record PaymentServiceRequest(String creditCardOwner, String cardNumber, YearMonth expirationDate, String cryptoCode,
double amount) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.github.lernejo.front.basket;

import java.time.YearMonth;

public record PaymentInformation(String lastname, String firstname, String cardNumber, YearMonth expirationDate,
String cryptoCode) {
}
Loading