diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java index 61d7281..5dd7102 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/CordaClient.java @@ -3,6 +3,8 @@ package djmil.cordacheckers.cordaclient; import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.UUID; +import java.util.concurrent.TimeUnit; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -12,25 +14,27 @@ import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import djmil.cordacheckers.cordaclient.dao.HoldingIdentity; import djmil.cordacheckers.cordaclient.dao.VirtualNode; import djmil.cordacheckers.cordaclient.dao.VirtualNodeList; import djmil.cordacheckers.cordaclient.dao.flow.RequestBody; +import djmil.cordacheckers.cordaclient.dao.flow.ResponseBody; import djmil.cordacheckers.cordaclient.dao.flow.arguments.Empty; @Service public class CordaClient { private final RestTemplate restTemplate; + private final ObjectMapper jsonMapper; - public CordaClient(RestTemplate restTemplate) { + public CordaClient(RestTemplate restTemplate, ObjectMapper jsonMapper) { //System.out.println("Creating REST Service"); // this.restTemplate = restTemplateBuilder // .basicAuthentication("admin", "admin") // .build(); this.restTemplate = restTemplate; + this.jsonMapper = jsonMapper; } public List getVirtualNodeList() { @@ -59,41 +63,121 @@ public class CordaClient { * Obtain list of unconsumed (active) GameProposals * @param holdingIdentity * @return GameProposals list in JSON form - * @throws JsonProcessingException */ - public String listGameProposals(HoldingIdentity holdingIdentity) throws JsonProcessingException { - // Request authorization header - HttpHeaders headers = basicAuthorizationHeader(); + public String listGameProposals(HoldingIdentity holdingIdentity) { - - - RequestBody body = new RequestBody("list-2", "djmil.cordacheckers.gameproposal.ListFlow", new Empty()); - - ObjectMapper mapper = new ObjectMapper(); + final RequestBody requestBody = new RequestBody( + "list-" + UUID.randomUUID(), + "djmil.cordacheckers.gameproposal.ListFlow", + new Empty() + ); - String json = mapper.writeValueAsString(body); + final String gameProposalsJsonString = cordaFlowExecute( + holdingIdentity, + requestBody + ); - if (json != null) - return json; - // new String("{\n" + // - // " \"clientRequestId\": \"list-1\",\n" + // - // " \"flowClassName\": \"djmil.cordacheckers.gameproposal.ListFlow\",\n" + // - // " \"requestBody\": {}\n" + // - // "}"); + return gameProposalsJsonString; + } - System.out.println("HiH "+holdingIdentity.shortHash()+"\nRequst JSON = "+json); + private String cordaFlowExecute(HoldingIdentity holdingIdentity, RequestBody requestBody) { + try { + final String requestBodyJson = this.jsonMapper.writeValueAsString(requestBody); + + final ResponseBody startedFlow = cordaFlowPost( + holdingIdentity, + requestBodyJson + ); + + final String flowExecutionResult = cordaFlowPoll(startedFlow); + + // NOTE: + // At this point, real production code, probably should convert data between CordaFlow + // abstarction into ReactApp abstraction. Instead, to limit boring json shuffling, all + // family of Corda.List flows were deliberatly designed to return frontend frendly JSONs. + // At the same time, all other Corda flows, simply return plain text string with + // operation result. + return flowExecutionResult; + } + catch (Exception e) { + throw new RuntimeException("Unable to perform "+requestBody.flowClassName() + +". Reason "+e.getMessage()); + } + } + + private ResponseBody cordaFlowPost(HoldingIdentity holdingIdentity, String requestBodyJson) { + final HttpHeaders requestHeaders = basicAuthorizationHeader(); - // Request - final HttpEntity request = new HttpEntity<>(json, headers); + final HttpEntity request = new HttpEntity<>(requestBodyJson, requestHeaders); - ResponseEntity resp = this.restTemplate.exchange( - "https://localhost:8888/api/v1/flow/"+holdingIdentity.shortHash(), + final ResponseEntity responce = this.restTemplate.exchange( + "https://localhost:8888/api/v1/flow/" + holdingIdentity.shortHash(), HttpMethod.POST, request, - String.class); + ResponseBody.class + ); - return resp.getBody(); + if (!responce.getStatusCode().is2xxSuccessful()) { + throw new RuntimeException("CordaClient.startCordaFlow: unexpected request status " + +responce.getStatusCode()) ; + } + + final ResponseBody responseBody = requireNonNull( + responce.getBody(), + "CordaClient.startCordaFlow: empty getBody()" + ); + + if (!responseBody.isFlowStarted()) { + throw new RuntimeException("CordaClient.startCordaFlow: Unexpected status: " + +responseBody.flowStatus() + ": " + +responseBody.flowError()); + } + + return responseBody; + } + + private String cordaFlowPoll(ResponseBody startedFlow) throws InterruptedException { + final HttpHeaders requestHeaders = basicAuthorizationHeader(); + + final HttpEntity request = new HttpEntity<>(requestHeaders); + + for (int retry = 0; retry < 6; retry++) { + // Give Corda cluster some time to process our request + TimeUnit.SECONDS.sleep(retry*retry +1); // 1 2 5 8 17 33 sec + + final ResponseEntity responce = this.restTemplate.exchange( + "https://localhost:8888/api/v1/flow/" + + startedFlow.holdingIdentityShortHash()+"/" + + startedFlow.clientRequestId(), + HttpMethod.GET, + request, + ResponseBody.class + ); + + if (responce.getStatusCode() != HttpStatus.OK) { + throw new RuntimeException("CordaClient.cordaFlowPoll: unexpected request status " + +responce.getStatusCode()) ; + } + + final ResponseBody responseBody = requireNonNull( + responce.getBody(), + "CordaClient.cordaFlowPoll: empty getBody()" + ); + + if (responseBody.isFlowCompleted() && responseBody.flowResult() != null) { + System.out.println("Completed "+responseBody.flowResult()); + return responseBody.flowResult(); + } else + if (responseBody.flowError() != null) { + return "Flow execution error: " +responseBody.flowError(); + } else + if (!responseBody.isFlowRunning()) { + return "Unexpect ResponseBody status: " +responseBody.flowStatus(); + } + } + + return "CordaClient.cordaFlowPoll: retry limit"; } private HttpHeaders basicAuthorizationHeader() { diff --git a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/ResponseBody.java b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/ResponseBody.java index f40e58a..47b6ee5 100644 --- a/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/ResponseBody.java +++ b/backend/src/main/java/djmil/cordacheckers/cordaclient/dao/flow/ResponseBody.java @@ -8,4 +8,18 @@ public record ResponseBody( String flowResult, String flowError, String timestamp -) { } +) { + + public boolean isFlowStarted() { + return this.flowStatus.equals("START_REQUESTED"); + } + + public boolean isFlowRunning() { + return this.flowStatus.equals("RUNNING"); + } + + public boolean isFlowCompleted() { + return this.flowStatus.equals("COMPLETED"); + } + +} diff --git a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java index 1166498..dfa83e7 100644 --- a/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java +++ b/backend/src/test/java/djmil/cordacheckers/cordaclient/CordaClientTest.java @@ -32,7 +32,8 @@ public class CordaClientTest { @Test void testListGameProposals() throws JsonProcessingException { - String resp = cordaClient.listGameProposals(holdingIdentityResolver.getByCommonName("alice")); + String resp = cordaClient.listGameProposals( + holdingIdentityResolver.getByCommonName("alice")); System.out.println("testListGameProposals "+ resp); }