Switched from single-node (assign) to multi-instance (subscribe)
[demos/kafka/demos-kafka-payment-system-transfer] / src / main / java / de / juplo / kafka / payment / transfer / adapter / TransferController.java
1 package de.juplo.kafka.payment.transfer.adapter;
2
3
4 import de.juplo.kafka.payment.transfer.domain.Transfer;
5 import de.juplo.kafka.payment.transfer.ports.GetTransferUseCase;
6 import de.juplo.kafka.payment.transfer.ports.MessagingService;
7 import lombok.RequiredArgsConstructor;
8 import lombok.extern.slf4j.Slf4j;
9 import org.springframework.http.HttpStatus;
10 import org.springframework.http.MediaType;
11 import org.springframework.http.ResponseEntity;
12 import org.springframework.validation.FieldError;
13 import org.springframework.web.bind.MethodArgumentNotValidException;
14 import org.springframework.web.bind.annotation.*;
15 import org.springframework.web.context.request.async.DeferredResult;
16
17 import javax.servlet.http.HttpServletRequest;
18 import javax.validation.Valid;
19 import java.net.URI;
20 import java.util.Date;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.Optional;
24 import java.util.concurrent.CompletableFuture;
25
26
27 @RequestMapping(TransferController.PATH)
28 @ResponseBody
29 @RequiredArgsConstructor
30 @Slf4j
31  public class TransferController
32 {
33   public final static String PATH = "/transfers";
34
35   private final GetTransferUseCase getTransferUseCase;
36   private final MessagingService messagingService;
37   private final TransferConsumer consumer;
38
39
40   @PostMapping(
41       path = "",
42       consumes = MediaType.APPLICATION_JSON_VALUE,
43       produces = MediaType.APPLICATION_JSON_VALUE)
44   public DeferredResult<ResponseEntity<?>> transfer(
45       HttpServletRequest request,
46       @Valid @RequestBody TransferDTO transferDTO)
47   {
48     DeferredResult<ResponseEntity<?>> result = new DeferredResult<>();
49
50     getTransferUseCase
51         .get(transferDTO.getId())
52         .map(transfer ->
53             CompletableFuture.completedFuture(
54                 ResponseEntity
55                     .ok()
56                     .location(location(transferDTO))
57                     .build()))
58         .or(() ->
59             Optional.of(
60                 messagingService
61                     .send(
62                         Transfer
63                             .builder()
64                             .id(transferDTO.getId())
65                             .payer(transferDTO.getPayer())
66                             .payee(transferDTO.getPayee())
67                             .amount(transferDTO.getAmount())
68                             .build())
69                     .thenApply($ ->
70                         ResponseEntity
71                             .created(location(transferDTO))
72                             .build())))
73         .get()
74         .thenAccept(responseEntity -> result.setResult(responseEntity))
75         .exceptionally(e ->
76         {
77           result.setErrorResult(e);
78           return null;
79         });
80
81     return result;
82   }
83
84   private URI location(TransferDTO transferDTO)
85   {
86     return URI.create(PATH + "/" + transferDTO.getId());
87   }
88
89   @GetMapping(
90       path = "/{id}",
91       produces = MediaType.APPLICATION_JSON_VALUE)
92   public ResponseEntity<TransferDTO> get(@PathVariable Long id)
93   {
94     return
95         consumer
96             .uriForKey(Long.toString(id))
97             .map(uri ->
98             {
99               ResponseEntity<TransferDTO> response =
100                   ResponseEntity
101                       .status(HttpStatus.TEMPORARY_REDIRECT)
102                       .location(URI.create(uri + PATH + "/" + id))
103                       .build();
104               return response;
105             })
106             .orElseGet(() ->
107                 getTransferUseCase
108                     .get(id)
109                     .map(transfer -> ResponseEntity.ok(TransferDTO.of(transfer)))
110                     .orElse(ResponseEntity.notFound().build()));
111   }
112
113   @ResponseStatus(HttpStatus.BAD_REQUEST)
114   @ExceptionHandler(MethodArgumentNotValidException.class)
115   public Map<String, Object> handleValidationExceptions(
116       HttpServletRequest request,
117       MethodArgumentNotValidException e)
118   {
119     Map<String, Object> errorAttributes = new HashMap<>();
120     errorAttributes.put("status", HttpStatus.BAD_REQUEST.value());
121     errorAttributes.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
122     errorAttributes.put("path", request.getRequestURI());
123     errorAttributes.put("method", request.getMethod());
124     errorAttributes.put("timestamp", new Date());
125     Map<String, String> errors = new HashMap<>();
126     e.getBindingResult().getAllErrors().forEach((error) -> {
127       String fieldName = ((FieldError) error).getField();
128       String errorMessage = error.getDefaultMessage();
129       errors.put(fieldName, errorMessage);
130     });
131     errorAttributes.put("errors", errors);
132     errorAttributes.put("message", "Validation failed: Invalid message format, error count: " + errors.size());
133     return errorAttributes;
134   }
135 }