From 8c963bf4563a75125387deb75b8662f43a36557c Mon Sep 17 00:00:00 2001 From: Dimitry Declercq Date: Sat, 13 Jan 2018 11:28:41 +0100 Subject: [PATCH] Example implementation for Aws API Gateway User can extend SpringBootApiGatewayRequestHandler instead of the generic SpringBootRequestHandler. It ties the code to AWS and the API Gateway, but at least it supports the incoming data fully. Fixes gh-111, closes gh-136 --- .../spring-cloud-function-adapter-aws/pom.xml | 2 +- .../SpringBootApiGatewayRequestHandler.java | 96 ++++++++++++++++ .../adapter/aws/SpringBootRequestHandler.java | 6 +- .../aws/SpringFunctionInitializer.java | 2 +- ...ringBootApiGatewayRequestHandlerTests.java | 108 ++++++++++++++++++ .../function-sample-aws/pom.xml | 2 +- 6 files changed, 212 insertions(+), 4 deletions(-) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandler.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandlerTests.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml index 015e78bfa..8bc840369 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml @@ -20,7 +20,7 @@ UTF-8 UTF-8 1.8 - 1.2.1 + 2.0.2 diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandler.java new file mode 100644 index 000000000..720ffd861 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandler.java @@ -0,0 +1,96 @@ +package org.springframework.cloud.function.adapter.aws; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.function.context.catalog.FunctionInspector; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.GenericMessage; + +import java.util.HashMap; +import java.util.Map; + +public class SpringBootApiGatewayRequestHandler extends SpringBootRequestHandler { + + @Autowired + private ObjectMapper mapper; + + @Autowired + private FunctionInspector inspector; + + public SpringBootApiGatewayRequestHandler(Class configurationClass) { + super(configurationClass); + } + + public SpringBootApiGatewayRequestHandler() { + super(); + } + + protected Object convertEvent(APIGatewayProxyRequestEvent event) { + Object body = deserializeBody(event.getBody()); + if (functionAcceptsMessage()) { + return new GenericMessage<>(body, getHeaders(event)); + } else { + return body; + } + } + + private boolean functionAcceptsMessage() { + return inspector.isMessage(function()); + } + + private Object deserializeBody(String json) { + try { + return mapper.readValue(json, getInputType()); + } catch (Exception e) { + throw new IllegalStateException("Cannot convert event", e); + } + } + + private MessageHeaders getHeaders(APIGatewayProxyRequestEvent event) { + Map headers = new HashMap<>(); + if (event.getHeaders() != null) { + headers.putAll(event.getHeaders()); + } + headers.put("request", event); + return new MessageHeaders(headers); + } + + protected APIGatewayProxyResponseEvent convertOutput(Object output) { + if (functionReturnsMessage(output)) { + Message message = (Message) output; + return new APIGatewayProxyResponseEvent() + .withStatusCode((Integer) message.getHeaders().getOrDefault("statusCode", 200)) + .withHeaders(toResponseHeaders(message.getHeaders())) + .withBody(serializeBody(message.getPayload())); + } else { + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody(serializeBody(output)); + + + } + } + + private boolean functionReturnsMessage(Object output) { + return output instanceof Message; + } + + private Map toResponseHeaders(MessageHeaders messageHeaders) { + Map responseHeaders = new HashMap<>(); + messageHeaders.forEach((key, value) -> responseHeaders.put(key, value.toString())); + return responseHeaders; + } + + private String serializeBody(Object body) { + try { + return mapper.writeValueAsString(body); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Cannot convert output", e); + } + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java index ec1bdec8f..e3385478e 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootRequestHandler.java @@ -49,7 +49,7 @@ public class SpringBootRequestHandler extends SpringFunctionInitializer im private Object result(Object input, Flux output) { List result = new ArrayList<>(); for (Object value : output.toIterable()) { - result.add(value); + result.add(convertOutput(value)); } if (isSingleValue(input) && result.size()==1) { return result.get(0); @@ -72,4 +72,8 @@ public class SpringBootRequestHandler extends SpringFunctionInitializer im return event; } + protected O convertOutput(Object output) { + return (O) output; + } + } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringFunctionInitializer.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringFunctionInitializer.java index 70ddc65be..7d03799de 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringFunctionInitializer.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringFunctionInitializer.java @@ -136,7 +136,7 @@ public class SpringFunctionInitializer implements Closeable { return Object.class; } - private Object function() { + protected Object function() { return this.function != null ? this.function : (this.consumer != null ? this.consumer : this.supplier); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandlerTests.java new file mode 100644 index 000000000..d89b9a79e --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootApiGatewayRequestHandlerTests.java @@ -0,0 +1,108 @@ +package org.springframework.cloud.function.adapter.aws; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.junit.Test; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SpringBootApiGatewayRequestHandlerTests { + + private SpringBootApiGatewayRequestHandler handler; + + @Test + public void functionBean() { + handler = new SpringBootApiGatewayRequestHandler(FunctionConfig.class); + handler.initialize(); + + APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent(); + request.setBody("{\"value\":\"foo\"}"); + + Object output = handler.handleRequest(request, null); + assertThat(output).isInstanceOf(APIGatewayProxyResponseEvent.class); + assertThat(((APIGatewayProxyResponseEvent) output).getStatusCode()).isEqualTo(200); + assertThat(((APIGatewayProxyResponseEvent) output).getBody()).isEqualTo("{\"value\":\"FOO\"}"); + } + + @Configuration + @Import({ContextFunctionCatalogAutoConfiguration.class, + JacksonAutoConfiguration.class}) + protected static class FunctionConfig { + @Bean + public Function function() { + return foo -> new Bar(foo.getValue().toUpperCase()); + } + } + + @Test + public void functionMessageBean() { + handler = new SpringBootApiGatewayRequestHandler(FunctionMessageConfig.class); + handler.initialize(); + + APIGatewayProxyRequestEvent request = new APIGatewayProxyRequestEvent(); + request.setBody("{\"value\":\"foo\"}"); + + Object output = handler.handleRequest(request, null); + assertThat(output).isInstanceOf(APIGatewayProxyResponseEvent.class); + assertThat(((APIGatewayProxyResponseEvent) output).getStatusCode()).isEqualTo(200); + assertThat(((APIGatewayProxyResponseEvent) output).getHeaders().get("spring")).isEqualTo("cloud"); + assertThat(((APIGatewayProxyResponseEvent) output).getBody()).isEqualTo("{\"value\":\"FOO\"}"); + } + + @Configuration + @Import({ContextFunctionCatalogAutoConfiguration.class, + JacksonAutoConfiguration.class}) + protected static class FunctionMessageConfig { + @Bean + public Function, Message> function() { + return (foo -> { + Map headers = Collections.singletonMap("spring", "cloud"); + return new GenericMessage<>( + new Bar(foo.getPayload().getValue().toUpperCase()), + headers); + }); + } + } + + protected static class Foo { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + protected static class Bar { + private String value; + + public Bar() { + } + + public Bar(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/spring-cloud-function-samples/function-sample-aws/pom.xml b/spring-cloud-function-samples/function-sample-aws/pom.xml index 8d148a709..66ebd9b17 100644 --- a/spring-cloud-function-samples/function-sample-aws/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws/pom.xml @@ -23,7 +23,7 @@ UTF-8 1.8 1.0.9.RELEASE - 1.2.1 + 2.0.2 3.1.2.RELEASE 1.0.0.BUILD-SNAPSHOT 1.0.0.BUILD-SNAPSHOT