From 27344d7098abf7d80b0d7d912babd398c7c692e2 Mon Sep 17 00:00:00 2001 From: Markus Gulden Date: Wed, 24 Apr 2019 22:22:10 -0400 Subject: [PATCH] GH-317, Implement HTTP request-specific handler for Azure adapter * Handler extends AzureSpringBootRequestHandler with types HttpRequestMessage and HttpResponseMessage (which are required by Azure Functions for HTTP-triggered functions) * Handler extracts request path, query string parameters as well as the HTTP method from the HttpRequestMessage object and forwards them to the function as message headers * Handler creates an HttpResponseMessage with the response coming from the called function * Created unit tests accordingly GH-317, Implement HTTP request-specific handler for Azure adapter * Handler extends AzureSpringBootRequestHandler with types HttpRequestMessage and HttpResponseMessage (which are required by Azure Functions for HTTP-triggered functions) * Handler extracts request path, query string parameters as well as the HTTP method from the HttpRequestMessage object and forwards them to the function as message headers * Handler creates an HttpResponseMessage with the response coming from the called function * Created unit tests accordingly GH-317, Implement HTTP request-specific handler for Azure adapter * Handler extends AzureSpringBootRequestHandler with types HttpRequestMessage and HttpResponseMessage (which are required by Azure Functions for HTTP-triggered functions) * Handler extracts request path, query string parameters as well as the HTTP method from the HttpRequestMessage object and forwards them to the function as message headers * Handler creates an HttpResponseMessage with the response coming from the called function * Created unit tests accordingly --- .../AzureSpringBootHttpRequestHandler.java | 166 ++++++++++ ...zureSpringBootHttpRequestHandlerTests.java | 288 ++++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java create mode 100644 spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandlerTests.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java new file mode 100644 index 000000000..8f5c83744 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java @@ -0,0 +1,166 @@ +/* + * Copyright 2017-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.adapter.azure; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.reactivestreams.Publisher; +import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.GenericMessage; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpResponseMessage.Builder; + +import reactor.core.publisher.Flux; + +/** + * @param input type + * @author Markus Gulden + */ +public class AzureSpringBootHttpRequestHandler extends + AzureSpringBootRequestHandler, HttpResponseMessage> { + + private HttpRequestMessage input; + + public AzureSpringBootHttpRequestHandler(Class configurationClass) { + super(configurationClass); + } + + public AzureSpringBootHttpRequestHandler() { + super(); + } + + @SuppressWarnings("rawtypes") + @Override + protected Object convertEvent(HttpRequestMessage event) { + + if (event.getBody() != null) { + if (functionAcceptsMessage()) { + return new GenericMessage(event.getBody(), getHeaders(event)); + } + else { + return event.getBody(); + } + } + else { + if (functionAcceptsMessage()) { + return new GenericMessage(Optional.empty(), getHeaders(event)); + } + else { + return Optional.empty(); + } + } + } + + protected boolean functionAcceptsMessage() { + + return this.getInspector().isMessage(function()); + } + + private MessageHeaders getHeaders(HttpRequestMessage event) { + Map headers = new HashMap(); + + if (event.getHeaders() != null) { + headers.putAll(event.getHeaders()); + } + if (event.getQueryParameters() != null) { + headers.putAll(event.getQueryParameters()); + } + if (event.getUri() != null) { + headers.put("path", event.getUri().getPath()); + } + + if (event.getHttpMethod() != null) { + headers.put("httpMethod", event.getHttpMethod().toString()); + } + + headers.put("request", event.getBody()); + return new MessageHeaders(headers); + } + + @Override + @SuppressWarnings("unchecked") + protected O result(Object input, Publisher output) { + + List result = new ArrayList<>(); + for (Object value : Flux.from(output).toIterable()) { + result.add(convertOutput(value)); + } + if (isSingleInput(getFunction(), input) && result.size() == 1) { + HttpResponseMessage value = (HttpResponseMessage) result.get(0); + return (O) value; + } + if (isSingleOutput(getFunction(), output) && result.size() == 1) { + HttpResponseMessage value = (HttpResponseMessage) result.get(0); + return (O) value; + } + + O value = (O) result; + return value; + } + + @Override + protected HttpResponseMessage convertOutput(Object output) { + if (functionReturnsMessage(output)) { + + Message message = (Message) output; + Builder builder = this.input + .createResponseBuilder(com.microsoft.azure.functions.HttpStatus.OK) + .body(message.getPayload()); + for (Map.Entry entry : message.getHeaders().entrySet()) { + builder = builder.header(entry.getKey(), entry.getValue().toString()); + } + return builder.build(); + } + else { + + return this.input + .createResponseBuilder(com.microsoft.azure.functions.HttpStatus.OK) + .body(output).build(); + } + } + + @Override + public HttpResponseMessage handleRequest(HttpRequestMessage event, + ExecutionContext context) { + this.input = event; + Object response = super.handleRequest(event, context); + if (returnsOutput()) { + return (HttpResponseMessage) response; + } + else { + return this.input + .createResponseBuilder(com.microsoft.azure.functions.HttpStatus.OK) + .build(); + } + } + + protected boolean returnsOutput() { + return !this.getInspector().getOutputType(function()).equals(Void.class); + } + + protected boolean functionReturnsMessage(Object output) { + return output instanceof Message; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandlerTests.java new file mode 100644 index 000000000..46a72d070 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandlerTests.java @@ -0,0 +1,288 @@ +/* + * Copyright 2012-2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.adapter.azure; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; +import com.microsoft.azure.functions.HttpResponseMessage.Builder; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.HttpStatusType; + +import org.junit.After; +import org.junit.Test; +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 static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Markus Gulden + */ +public class AzureSpringBootHttpRequestHandlerTests { + + private AzureSpringBootHttpRequestHandler handler = null; + + AzureSpringBootHttpRequestHandler handler(Class config) { + AzureSpringBootHttpRequestHandler handler = new AzureSpringBootHttpRequestHandler( + config); + this.handler = handler; + return handler; + } + + @Test + public void testWithBody() { + AzureSpringBootHttpRequestHandler handler = handler( + FunctionMessageBodyConfig.class); + HttpRequestMessageStub request = new HttpRequestMessageStub(); + request.setBody(new Foo("foo")); + + HttpResponseMessage response = handler.handleRequest(request, + new TestExecutionContext("uppercase")); + + assertThat(response.getBody()).isInstanceOf(Bar.class); + assertThat(response.getStatusCode()).isEqualTo(200); + Bar body = (Bar) response.getBody(); + assertThat(body.getValue()).isEqualTo("FOO"); + } + + @Test + public void testWithRequestParameters() throws URISyntaxException { + AzureSpringBootHttpRequestHandler handler = handler( + FunctionMessageEchoReqParametersConfig.class); + HttpRequestMessageStub request = new HttpRequestMessageStub(); + request.setUri(new URI("http://localhost:8080/pathValue")); + request.setHeaders(Collections.singletonMap("test-header", "headerValue")); + request.setQueryParameters(Collections.singletonMap("query", "queryValue")); + request.setHttpMethod(HttpMethod.GET); + + HttpResponseMessage response = handler.handleRequest(request, + new TestExecutionContext("uppercase")); + + assertThat(response.getStatusCode()).isEqualTo(200); + assertThat(response.getHeader("path")).isEqualTo("/pathValue"); + assertThat(response.getHeader("query")).isEqualTo("queryValue"); + assertThat(response.getHeader("test-header")).isEqualTo("headerValue"); + Bar body = (Bar) response.getBody(); + assertThat(body.getValue()).isEqualTo("body"); + } + + @Test + public void testWithEmptyBody() { + AzureSpringBootHttpRequestHandler handler = handler( + FunctionMessageConsumerConfig.class); + HttpRequestMessageStub request = new HttpRequestMessageStub(); + + HttpResponseMessage response = handler.handleRequest(request, + new TestExecutionContext("uppercase")); + + assertThat(response.getStatusCode()).isEqualTo(200); + Bar body = (Bar) response.getBody(); + assertThat(body).isNull(); + } + + @After + public void close() throws IOException { + if (this.handler != null) { + this.handler.close(); + } + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class FunctionMessageBodyConfig { + + @Bean + public Function, Message> function() { + return (foo -> { + Map headers = new HashMap<>(); + return new GenericMessage<>( + new Bar(foo.getPayload().getValue().toUpperCase()), headers); + }); + } + + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class FunctionMessageEchoReqParametersConfig { + + @Bean + public Function, Message> function() { + return (message -> { + Map headers = new HashMap<>(); + headers.put("path", message.getHeaders().get("path")); + headers.put("query", message.getHeaders().get("query")); + headers.put("test-header", message.getHeaders().get("test-header")); + headers.put("httpMethod", message.getHeaders().get("httpMethod")); + return new GenericMessage<>(new Bar("body"), headers); + }); + } + + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class FunctionMessageConsumerConfig { + + @Bean + public Consumer> function() { + return (foo -> { + }); + } + + } + + class HttpRequestMessageStub implements HttpRequestMessage { + + private URI uri; + private HttpMethod httpMethod; + private Map headers; + private Map queryParameters; + private I body; + + public void setUri(URI uri) { + this.uri = uri; + } + + public void setHttpMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public void setQueryParameters(Map queryParameters) { + this.queryParameters = queryParameters; + } + + public void setBody(I body) { + this.body = body; + } + + @Override + public URI getUri() { + return this.uri; + } + + @Override + public HttpMethod getHttpMethod() { + return this.httpMethod; + } + + @Override + public Map getHeaders() { + return this.headers; + } + + @Override + public Map getQueryParameters() { + return this.queryParameters; + } + + @Override + public I getBody() { + return this.body; + } + + @Override + public HttpResponseMessage.Builder createResponseBuilder(HttpStatusType status) { + return new BuilderStub().status(status); + } + + @Override + public Builder createResponseBuilder(HttpStatus status) { + return new BuilderStub().status(status); + } + + } + + class BuilderStub implements Builder { + + private HttpStatusType status; + private Map headers = new HashMap<>(); + private Object body; + + @Override + public Builder status(HttpStatusType status) { + this.status = status; + return this; + } + + @Override + public Builder header(String key, String value) { + headers.put(key, value); + return this; + } + + @Override + public Builder body(Object body) { + this.body = body; + return this; + } + + @Override + public HttpResponseMessage build() { + return new HttpResponseMessageStub(this.status, this.headers, this.body); + } + + } + + class HttpResponseMessageStub implements HttpResponseMessage { + + private HttpStatusType status; + private Map headers = new HashMap<>(); + private Object body; + + public HttpResponseMessageStub(HttpStatusType status, Map headers, + Object body) { + this.status = status; + this.headers = headers; + this.body = body; + } + + @Override + public HttpStatusType getStatus() { + return this.status; + } + + @Override + public String getHeader(String key) { + return this.headers.get(key); + } + + @Override + public Object getBody() { + return this.body; + } + + } +} \ No newline at end of file