From 3ca29ce2ec4971905492f6174da6116c48f3c8fa Mon Sep 17 00:00:00 2001 From: Matthew Horridge Date: Sat, 15 Jun 2024 11:24:36 -0700 Subject: [PATCH] Fix forms download by providing an endpoint in the api gateway --- pom.xml | 2 +- .../webprotege/gateway/FormsController.java | 34 +++++++++ .../protege/webprotege/gateway/RpcClient.java | 74 ++++++++++++++++++ .../webprotege/gateway/RpcClientTest.java | 76 +++++++++++++++++++ 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/main/java/edu/stanford/protege/webprotege/gateway/FormsController.java create mode 100644 src/main/java/edu/stanford/protege/webprotege/gateway/RpcClient.java create mode 100644 src/test/java/edu/stanford/protege/webprotege/gateway/RpcClientTest.java diff --git a/pom.xml b/pom.xml index bdc45b2..cff4b50 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ edu.stanford.protege webprotege-gwt-api-gateway - 1.0.7 + 1.0.8-SNAPSHOT webprotege-gwt-api-gateway The API Gateway for the WebProtégé GWT User Interface diff --git a/src/main/java/edu/stanford/protege/webprotege/gateway/FormsController.java b/src/main/java/edu/stanford/protege/webprotege/gateway/FormsController.java new file mode 100644 index 0000000..08bfa38 --- /dev/null +++ b/src/main/java/edu/stanford/protege/webprotege/gateway/FormsController.java @@ -0,0 +1,34 @@ +package edu.stanford.protege.webprotege.gateway; + +import edu.stanford.protege.webprotege.common.*; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +/** + * Matthew Horridge + * Stanford Center for Biomedical Informatics Research + * 2024-06-14 + */ +@RestController +public class FormsController { + + private static final RpcMethod GET_FORM_DESCRIPTORS = new RpcMethod("webprotege.forms.GetProjectFormDescriptors"); + + private static final String PROJECT_ID = "projectId"; + + private final RpcClient rpcClient; + + public FormsController(RpcClient rpcClient) { + this.rpcClient = rpcClient; + } + + @GetMapping("/data/projects/{projectId}/forms") + public ResponseEntity> getForms(@PathVariable(PROJECT_ID) ProjectId projectId, + @AuthenticationPrincipal Jwt jwt) { + return rpcClient.call(jwt, GET_FORM_DESCRIPTORS, Map.of(PROJECT_ID, projectId)); + } +} diff --git a/src/main/java/edu/stanford/protege/webprotege/gateway/RpcClient.java b/src/main/java/edu/stanford/protege/webprotege/gateway/RpcClient.java new file mode 100644 index 0000000..c02512b --- /dev/null +++ b/src/main/java/edu/stanford/protege/webprotege/gateway/RpcClient.java @@ -0,0 +1,74 @@ +package edu.stanford.protege.webprotege.gateway; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import edu.stanford.protege.webprotege.common.UserId; +import org.jetbrains.annotations.NotNull; +import org.springframework.http.*; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.concurrent.ExecutionException; + +/** + * Matthew Horridge + * Stanford Center for Biomedical Informatics Research + * 2024-06-14 + */ +@Component +public class RpcClient { + + private static final String PREFERRED_USERNAME = "preferred_username"; + + private final RpcRequestProcessor requestProcessor; + + private final ObjectMapper objectMapper; + + public RpcClient(RpcRequestProcessor requestProcessor, ObjectMapper objectMapper) { + this.requestProcessor = requestProcessor; + this.objectMapper = objectMapper; + } + + /** + * Make a call using the specified RPC method and parameters + * @param token The access token that is used for authentication and authorization + * @param method The RPC method to call + * @param params The parameters for the method + * @return A response entity that represents the result, if successful, or an error if + * there was a failure. + */ + @Nonnull + public ResponseEntity> call(@Nonnull Jwt token, + @Nonnull RpcMethod method, + @Nonnull Map params) { + var userIdClaim = token.getClaimAsString(PREFERRED_USERNAME); + var userId = UserId.valueOf(userIdClaim); + var paramsAsNode = serializeParams(params); + var rpcResponse = executeCall(token, method, paramsAsNode, userId); + var error = rpcResponse.error(); + if(error != null) { + throw new ResponseStatusException(error.code(), error.message(), null); + } + return ResponseEntity.ok(rpcResponse.result()); + + } + + private RpcResponse executeCall(@NotNull Jwt token, @NotNull RpcMethod method, ObjectNode paramsAsNode, UserId userId) { + try { + var response = requestProcessor.processRequest(new RpcRequest(method, paramsAsNode), + token.getTokenValue(), userId); + return response.get(); + } catch (ExecutionException e) { + throw new ResponseStatusException(500, e.getCause().getMessage(), e.getCause()); + } catch (Throwable t) { + throw new ResponseStatusException(500, t.getMessage(), t); + } + } + + private ObjectNode serializeParams(Map params) { + return objectMapper.convertValue(params, ObjectNode.class); + } +} diff --git a/src/test/java/edu/stanford/protege/webprotege/gateway/RpcClientTest.java b/src/test/java/edu/stanford/protege/webprotege/gateway/RpcClientTest.java new file mode 100644 index 0000000..9ec9d15 --- /dev/null +++ b/src/test/java/edu/stanford/protege/webprotege/gateway/RpcClientTest.java @@ -0,0 +1,76 @@ +package edu.stanford.protege.webprotege.gateway; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.*; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.server.ResponseStatusException; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class RpcClientTest { + + private RpcClient client; + + @Mock + private RpcRequestProcessor requestProcessor; + + @Mock + ObjectMapper objectMapper; + + + @BeforeEach + void setUp() { + client = new RpcClient(requestProcessor, + objectMapper); + } + + @Test + void shouldReturn200OkResult() { + // Given a normal completion + var resultMap = Map.of("foo", "bar"); + var response = RpcResponse.forResult("theMethod", resultMap); + var future = CompletableFuture.completedFuture(response); + when(requestProcessor.processRequest(any(), any(), any())) + .thenReturn(future); + var result = client.call(mock(Jwt.class), mock(RpcMethod.class), Map.of()); + // Result is 200 OK + assertThat(result.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200)); + assertThat(result.getBody()).isEqualTo(resultMap); + } + + @Test + void shouldThrowExceptionOnRpcErrorCode() { + // Given an error completion + var response = RpcResponse.forError("TheErrorMessage", HttpStatus.valueOf(400)); + var future = CompletableFuture.completedFuture(response); + when(requestProcessor.processRequest(any(), any(), any())) + .thenReturn(future); + var thrown = assertThrowsExactly(ResponseStatusException.class, () -> { + client.call(mock(Jwt.class), mock(RpcMethod.class), Map.of()); + }); + assertThat(thrown.getStatusCode().value()).isEqualTo(400); + } + + @Test + void shouldThrow500ErrorResultOnInternalErrorFromCompletion() { + // Given an error completion + when(requestProcessor.processRequest(any(), any(), any())) + .thenThrow(new RuntimeException("Some internal error")); + var thrown = assertThrowsExactly(ResponseStatusException.class, () -> { + client.call(mock(Jwt.class), mock(RpcMethod.class), Map.of()); + }); + assertThat(thrown.getStatusCode().value()).isEqualTo(500); + + } +} \ No newline at end of file