WIP
[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 import org.springframework.web.reactive.function.client.WebClient;
17 import reactor.core.publisher.Mono;
18
19 import javax.servlet.http.HttpServletRequest;
20 import javax.validation.Valid;
21 import java.net.URI;
22 import java.util.Date;
23 import java.util.HashMap;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.concurrent.CompletableFuture;
27
28
29 @RequestMapping(TransferController.PATH)
30 @ResponseBody
31 @RequiredArgsConstructor
32 @Slf4j
33  public class TransferController
34 {
35   public final static String PATH = "/transfers";
36
37   private final GetTransferUseCase getTransferUseCase;
38   private final MessagingService messagingService;
39   private final TransferConsumer consumer;
40   private final WebClient webClient;
41
42
43   @PostMapping(
44       path = "",
45       consumes = MediaType.APPLICATION_JSON_VALUE,
46       produces = MediaType.APPLICATION_JSON_VALUE)
47   public DeferredResult<ResponseEntity<?>> transfer(@Valid @RequestBody TransferDTO transferDTO)
48   {
49     DeferredResult<ResponseEntity<?>> result = new DeferredResult<>();
50
51     Long id = transferDTO.getId();
52
53     consumer
54         .uriForKey(id.toString())
55         .map(uri ->
56             webClient.get()
57                 .uri(uri + PATH + "/" + id)
58                 .accept(MediaType.APPLICATION_JSON)
59                 .retrieve()
60                 .onStatus(status -> true, bar -> Mono.empty())
61                 .toBodilessEntity()
62                 .blockOptional()
63                 .flatMap(resp ->
64                     resp.getStatusCode().is2xxSuccessful()
65                         ? Optional.of(Boolean.TRUE)
66                         : Optional.<Boolean>empty()))
67         .or(() -> Optional.of(getTransferUseCase.get(transferDTO.getId()).map(transfer -> Boolean.TRUE)))
68         .flatMap(optional -> optional)
69         .map($ ->
70             CompletableFuture.completedFuture(
71                 ResponseEntity
72                     .ok()
73                     .location(location(transferDTO))
74                     .build()))
75         .or(() ->
76             Optional.of(
77                 messagingService
78                     .send(
79                         Transfer
80                             .builder()
81                             .id(transferDTO.getId())
82                             .payer(transferDTO.getPayer())
83                             .payee(transferDTO.getPayee())
84                             .amount(transferDTO.getAmount())
85                             .build())
86                     .thenApply($ ->
87                         ResponseEntity
88                             .created(location(transferDTO))
89                             .build())))
90         .get()
91         .thenAccept(responseEntity -> result.setResult(responseEntity))
92         .exceptionally(e ->
93         {
94           result.setErrorResult(e);
95           return null;
96         });
97
98     return result;
99   }
100
101   private URI location(TransferDTO transferDTO)
102   {
103     return URI.create(PATH + "/" + transferDTO.getId());
104   }
105
106   @GetMapping(
107       path = "/{id}",
108       produces = MediaType.APPLICATION_JSON_VALUE)
109   public ResponseEntity<TransferDTO> get(@PathVariable Long id)
110   {
111     return
112         consumer
113             .uriForKey(Long.toString(id))
114             .map(uri ->
115             {
116               ResponseEntity<TransferDTO> response =
117                   ResponseEntity
118                       .status(HttpStatus.TEMPORARY_REDIRECT)
119                       .location(URI.create(uri + PATH + "/" + id))
120                       .build();
121               return response;
122             })
123             .orElseGet(() ->
124                 getTransferUseCase
125                     .get(id)
126                     .map(transfer -> ResponseEntity.ok(TransferDTO.of(transfer)))
127                     .orElse(ResponseEntity.notFound().build()));
128   }
129
130   @ResponseStatus(HttpStatus.BAD_REQUEST)
131   @ExceptionHandler(MethodArgumentNotValidException.class)
132   public Map<String, Object> handleValidationExceptions(
133       HttpServletRequest request,
134       MethodArgumentNotValidException e)
135   {
136     Map<String, Object> errorAttributes = new HashMap<>();
137     errorAttributes.put("status", HttpStatus.BAD_REQUEST.value());
138     errorAttributes.put("error", HttpStatus.BAD_REQUEST.getReasonPhrase());
139     errorAttributes.put("path", request.getRequestURI());
140     errorAttributes.put("method", request.getMethod());
141     errorAttributes.put("timestamp", new Date());
142     Map<String, String> errors = new HashMap<>();
143     e.getBindingResult().getAllErrors().forEach((error) -> {
144       String fieldName = ((FieldError) error).getField();
145       String errorMessage = error.getDefaultMessage();
146       errors.put(fieldName, errorMessage);
147     });
148     errorAttributes.put("errors", errors);
149     errorAttributes.put("message", "Validation failed: Invalid message format, error count: " + errors.size());
150     return errorAttributes;
151   }
152 }