diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureFunctionUtil.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureFunctionUtil.java new file mode 100644 index 000000000..621a8e532 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureFunctionUtil.java @@ -0,0 +1,115 @@ +/* + * Copyright 2023-2023 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.HashMap; + import java.util.Map; + import java.util.Optional; + + import com.microsoft.azure.functions.ExecutionContext; + import com.microsoft.azure.functions.HttpMethod; + import com.microsoft.azure.functions.HttpRequestMessage; + import org.reactivestreams.Publisher; + import reactor.core.publisher.Flux; + + import org.springframework.messaging.Message; + import org.springframework.messaging.MessageHeaders; + import org.springframework.messaging.support.MessageBuilder; + + /** + * + * @author Christian Tzolov + * @author Oleg Zhurakousky + * @author Chris Bono + * @since 4.0 + * + */ + public class AzureFunctionUtil { + + public static String EXECUTION_CONTEXT = "executionContext"; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Object enhanceInputIfNecessary(Object input, ExecutionContext executionContext) { + if (input == null) { // Supplier + return input; + } + if (input instanceof Publisher) { + return Flux.from((Publisher) input).map(item -> { + if (item instanceof Message) { + return MessageBuilder.fromMessage((Message) item) + .setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build(); + } + else { + return constructInputMessageFromItem(input, executionContext); + } + }); + } + else if (input instanceof Message) { + return MessageBuilder.fromMessage((Message) input) + .setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build(); + } + else if (input instanceof Iterable) { + return Flux.fromIterable((Iterable) input).map(item -> { + return constructInputMessageFromItem(item, executionContext); + }); + } + return constructInputMessageFromItem(input, executionContext); + } + + private static Message constructInputMessageFromItem(Object input, ExecutionContext executionContext) { + MessageBuilder messageBuilder = null; + if (input instanceof HttpRequestMessage) { + HttpRequestMessage requestMessage = (HttpRequestMessage) input; + Object payload = requestMessage.getHttpMethod() != null + && requestMessage.getHttpMethod().equals(HttpMethod.GET) + ? requestMessage.getQueryParameters() + : requestMessage.getBody(); + + if (payload == null) { + payload = Optional.empty(); + } + messageBuilder = MessageBuilder.withPayload(payload).copyHeaders(getHeaders(requestMessage)); + } + else { + messageBuilder = MessageBuilder.withPayload(input); + } + return messageBuilder.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build(); + } + + private static 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); + } + + } + \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java index c21560a44..17f912839 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java @@ -160,7 +160,7 @@ public class HttpFunctionInvokerTests { } - class HttpRequestMessageStub implements HttpRequestMessage { + public static class HttpRequestMessageStub implements HttpRequestMessage { private URI uri; private HttpMethod httpMethod; @@ -225,7 +225,7 @@ public class HttpFunctionInvokerTests { } - class BuilderStub implements Builder { + public static class BuilderStub implements Builder { private HttpStatusType status; private Map headers = new HashMap<>(); @@ -256,7 +256,7 @@ public class HttpFunctionInvokerTests { } - class HttpResponseMessageStub implements HttpResponseMessage { + public static class HttpResponseMessageStub implements HttpResponseMessage { private HttpStatusType status; private Map headers = new HashMap<>(); diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java index eaf61617a..23c1a14ed 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java @@ -14,103 +14,144 @@ * limitations under the License. */ -package org.springframework.cloud.function.adapter.azure.injector; + package org.springframework.cloud.function.adapter.azure.injector; -import java.util.Iterator; -import java.util.Optional; -import java.util.ServiceLoader; -import java.util.function.Function; - -import com.microsoft.azure.functions.ExecutionContext; -import com.microsoft.azure.functions.HttpMethod; -import com.microsoft.azure.functions.HttpRequestMessage; -import com.microsoft.azure.functions.annotation.AuthorizationLevel; -import com.microsoft.azure.functions.annotation.FunctionName; -import com.microsoft.azure.functions.annotation.HttpTrigger; -import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter; -import org.springframework.boot.context.TypeExcludeFilter; -import org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.ComponentScan.Filter; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.FilterType; - -/** - * @author Christian Tzolov - */ -public class AzureFunctionInstanceInjectorTest { - - @Test - public void testFunctionInjector() throws Exception { - - FunctionInstanceInjector injector = initializeFunctionInstanceInjector(); - Assertions.assertThat(injector).isNotNull(); - Assertions.assertThat(injector).isInstanceOf(AzureFunctionInstanceInjector.class); - - System.setProperty("MAIN_CLASS", MyMainConfig.class.getName()); - - MyAzureTestFunction functionInstance = injector.getInstance(MyAzureTestFunction.class); - Assertions.assertThat(functionInstance).isNotNull(); - Assertions.assertThat(functionInstance).isInstanceOf(MyAzureTestFunction.class); - } - - private static FunctionInstanceInjector initializeFunctionInstanceInjector() { - FunctionInstanceInjector functionInstanceInjector = null; - ClassLoader prevContextClassLoader = Thread.currentThread().getContextClassLoader(); - try { - Iterator iterator = ServiceLoader.load(FunctionInstanceInjector.class).iterator(); - if (iterator.hasNext()) { - functionInstanceInjector = iterator.next(); - if (iterator.hasNext()) { - throw new RuntimeException( - "Customer function app has multiple FunctionInstanceInjector implementations"); - } - } - else { - functionInstanceInjector = new FunctionInstanceInjector() { - @Override - public T getInstance(Class functionClass) throws Exception { - return functionClass.getDeclaredConstructor().newInstance(); - } - }; - } - } - finally { - Thread.currentThread().setContextClassLoader(prevContextClassLoader); - } - return functionInstanceInjector; - } - - @Configuration - @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), - @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) - public static class MyMainConfig { - - @Bean - public Function uppercase() { - return message -> message.toUpperCase(); - } - } - - @Configuration - public static class MyAzureTestFunction { - - @Autowired - private Function uppercase; - - @FunctionName("ditest") - public String execute( - @HttpTrigger(name = "req", methods = { HttpMethod.GET, - HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, - ExecutionContext context) { - - return uppercase.apply(request.getBody().get()); - } - } -} + import java.util.Iterator; + import java.util.Optional; + import java.util.ServiceLoader; + import java.util.function.Function; + import java.util.logging.Logger; + + import com.microsoft.azure.functions.ExecutionContext; + import com.microsoft.azure.functions.HttpMethod; + import com.microsoft.azure.functions.HttpRequestMessage; + import com.microsoft.azure.functions.annotation.AuthorizationLevel; + import com.microsoft.azure.functions.annotation.FunctionName; + import com.microsoft.azure.functions.annotation.HttpTrigger; + import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector; + import org.assertj.core.api.Assertions; + import org.junit.jupiter.api.Test; + + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.boot.autoconfigure.AutoConfigurationExcludeFilter; + import org.springframework.boot.context.TypeExcludeFilter; + import org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector; + import org.springframework.cloud.function.adapter.azure.AzureFunctionUtil; + import org.springframework.cloud.function.adapter.azure.HttpFunctionInvokerTests; + import org.springframework.context.annotation.Bean; + import org.springframework.context.annotation.ComponentScan; + import org.springframework.context.annotation.ComponentScan.Filter; + import org.springframework.context.annotation.Configuration; + import org.springframework.context.annotation.FilterType; + import org.springframework.messaging.Message; + + /** + * @author Christian Tzolov + */ + public class AzureFunctionInstanceInjectorTest { + + static ExecutionContext executionContext = new ExecutionContext() { + @Override + public Logger getLogger() { + return Logger.getLogger(AzureFunctionInstanceInjectorTest.class.getName()); + } + + @Override + public String getInvocationId() { + return "id1"; + } + + @Override + public String getFunctionName() { + return "hello"; + } + }; + + @Test + public void testFunctionInjector() throws Exception { + + FunctionInstanceInjector injector = initializeFunctionInstanceInjector(); + Assertions.assertThat(injector).isNotNull(); + Assertions.assertThat(injector).isInstanceOf(AzureFunctionInstanceInjector.class); + + System.setProperty("MAIN_CLASS", MyMainConfig.class.getName()); + + MyAzureTestFunction functionInstance = injector.getInstance(MyAzureTestFunction.class); + + HttpFunctionInvokerTests.HttpRequestMessageStub> request = new HttpFunctionInvokerTests.HttpRequestMessageStub>(); + + request.setBody(Optional.of("test")); + + String result = functionInstance.execute(request, executionContext); + + Assertions.assertThat(result).isEqualTo("TEST"); + + Assertions.assertThat(functionInstance).isNotNull(); + Assertions.assertThat(functionInstance).isInstanceOf(MyAzureTestFunction.class); + } + + private static FunctionInstanceInjector initializeFunctionInstanceInjector() { + FunctionInstanceInjector functionInstanceInjector = null; + ClassLoader prevContextClassLoader = Thread.currentThread().getContextClassLoader(); + try { + Iterator iterator = ServiceLoader.load(FunctionInstanceInjector.class).iterator(); + if (iterator.hasNext()) { + functionInstanceInjector = iterator.next(); + if (iterator.hasNext()) { + throw new RuntimeException( + "Customer function app has multiple FunctionInstanceInjector implementations"); + } + } + else { + functionInstanceInjector = new FunctionInstanceInjector() { + @Override + public T getInstance(Class functionClass) throws Exception { + return functionClass.getDeclaredConstructor().newInstance(); + } + }; + } + } + finally { + Thread.currentThread().setContextClassLoader(prevContextClassLoader); + } + return functionInstanceInjector; + } + + @Configuration + @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) + public static class MyMainConfig { + + @Bean + public Function, String> uppercase() { + return message -> { + ExecutionContext context = (ExecutionContext) message.getHeaders() + .get(AzureFunctionUtil.EXECUTION_CONTEXT); + Assertions.assertThat(context).isNotNull(); + Assertions.assertThat(context.getFunctionName()).isEqualTo("hello"); + return message.getPayload().toUpperCase(); + }; + + } + } + + @Configuration + public static class MyAzureTestFunction { + + @Autowired + private Function, String> uppercase; + + @FunctionName("ditest") + public String execute( + @HttpTrigger(name = "req", methods = { HttpMethod.GET, + HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + ExecutionContext context) { + + Message enhancedRequest = (Message) AzureFunctionUtil.enhanceInputIfNecessary( + request.getBody().get(), + context); + return uppercase.apply(enhancedRequest); + } + } + } + \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/pom.xml b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/pom.xml index fcecfd3e2..3675daf52 100644 --- a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/pom.xml +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/pom.xml @@ -33,7 +33,7 @@ org.springframework.cloud spring-cloud-function-adapter-azure - 4.0.0-SNAPSHOT + 4.0.0 diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/pom.xml b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/pom.xml index 05aec60be..57ed7c102 100644 --- a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/pom.xml +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/pom.xml @@ -36,7 +36,7 @@ org.springframework.cloud spring-cloud-function-adapter-azure - 4.0.0-SNAPSHOT + 4.0.0 diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/pom.xml b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/pom.xml index 08ecdf528..06b0160db 100644 --- a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/pom.xml +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/pom.xml @@ -34,7 +34,7 @@ org.springframework.cloud spring-cloud-function-adapter-azure - 4.0.0-SNAPSHOT + 4.0.0