diff --git a/.github/dco.yml b/.github/dco.yml
new file mode 100644
index 000000000..0c4b142e9
--- /dev/null
+++ b/.github/dco.yml
@@ -0,0 +1,2 @@
+require:
+ members: false
diff --git a/README.adoc b/README.adoc
index cb06b375d..8df5028bc 100644
--- a/README.adoc
+++ b/README.adoc
@@ -31,21 +31,17 @@ tracker for issues and merging pull requests into main. If you want
to contribute even something trivial please do not hesitate, but
follow the guidelines below.
-[[sign-the-contributor-license-agreement]]
-== Sign the Contributor License Agreement
+[[developer-certificate-of-origin]]
+== Developer Certificate of Origin (DCO)
-Before we accept a non-trivial patch or pull request we will need you to sign the
-https://cla.pivotal.io/sign/spring[Contributor License Agreement].
-Signing the contributor's agreement does not grant anyone commit rights to the main
-repository, but it does mean that we can accept your contributions, and you will get an
-author credit if we do. Active contributors might be asked to join the core team, and
-given the ability to merge pull requests.
+All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
+For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring].
[[code-of-conduct]]
== Code of Conduct
-This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/src/main/asciidoc/code-of-conduct.adoc[code of
+This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/modules/ROOT/partials/code-of-conduct.adoc[code of
conduct]. By participating, you are expected to uphold this code. Please report
-unacceptable behavior to spring-code-of-conduct@pivotal.io.
+unacceptable behavior to code-of-conduct@spring.io.
[[code-conventions-and-housekeeping]]
== Code Conventions and Housekeeping
@@ -138,7 +134,7 @@ Checkstyle rules are *disabled by default*. To add checkstyle to your project ju
If you need to suppress some rules (e.g. line length needs to be longer), then it's enough for you to define a file under `${project.root}/src/checkstyle/checkstyle-suppressions.xml` with your suppressions. Example:
-.projectRoot/src/checkstyle/checkstyle-suppresions.xml
+.projectRoot/src/checkstyle/checkstyle-suppressions.xml
----
org.springframework.cloud
spring-cloud-function-parent
- 4.1.4-SNAPSHOT
+ 4.1.7-SNAPSHOT
jar
Spring Cloud Function Docs
diff --git a/pom.xml b/pom.xml
index d31406aa6..5d1d4af40 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,13 +6,13 @@
spring-cloud-function-parent
Spring Cloud Function Parent
- 4.1.4-SNAPSHOT
+ 4.1.7-SNAPSHOT
pom
org.springframework.cloud
spring-cloud-build
- 4.1.4-SNAPSHOT
+ 4.1.7-SNAPSHOT
diff --git a/spring-cloud-function-adapters/pom.xml b/spring-cloud-function-adapters/pom.xml
index 972ac286b..dc5d1e4de 100644
--- a/spring-cloud-function-adapters/pom.xml
+++ b/spring-cloud-function-adapters/pom.xml
@@ -10,7 +10,7 @@
org.springframework.cloud
spring-cloud-function-parent
- 4.1.4-SNAPSHOT
+ 4.1.7-SNAPSHOT
spring-cloud-function-adapter-parent
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 040ba71a1..bc0b31faf 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
@@ -13,13 +13,13 @@
org.springframework.cloud
spring-cloud-function-adapter-parent
- 4.1.4-SNAPSHOT
+ 4.1.7-SNAPSHOT
UTF-8
UTF-8
- 3.11.4
+ 3.14.0
1.12.29
1.0.1
1.1.5
diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java
index 9c5c0bd39..7077a1595 100644
--- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java
+++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java
@@ -17,11 +17,14 @@
package org.springframework.cloud.function.adapter.aws;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer;
import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers;
+import com.amazonaws.services.lambda.runtime.serialization.events.serializers.S3EventSerializer;
import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils;
import org.springframework.cloud.function.context.config.JsonMessageConverter;
@@ -30,6 +33,7 @@
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.MessageConverter;
+import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
/**
@@ -44,6 +48,9 @@ class AWSTypesMessageConverter extends JsonMessageConverter {
private final JsonMapper jsonMapper;
+ @SuppressWarnings("rawtypes")
+ private final AtomicReference s3EventSerializer = new AtomicReference<>();
+
AWSTypesMessageConverter(JsonMapper jsonMapper) {
this(jsonMapper, new MimeType("application", "json"), new MimeType(CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getType(),
CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getSubtype() + "+json"));
@@ -75,7 +82,6 @@ protected Object convertFromInternal(Message> message, Class> targetClass, @
if (message.getPayload().getClass().isAssignableFrom(targetClass)) {
return message.getPayload();
}
-
if (targetClass.getPackage() != null &&
targetClass.getPackage().getName().startsWith("com.amazonaws.services.lambda.runtime.events")) {
PojoSerializer> serializer = LambdaEventSerializers.serializerFor(targetClass, Thread.currentThread().getContextClassLoader());
@@ -110,12 +116,23 @@ protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers)
}
+ @SuppressWarnings("unchecked")
@Override
protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers,
@Nullable Object conversionHint) {
if (payload instanceof String && headers.containsKey(AWSLambdaUtils.IS_BASE64_ENCODED) && (boolean) headers.get(AWSLambdaUtils.IS_BASE64_ENCODED)) {
return ((String) payload).getBytes(StandardCharsets.UTF_8);
}
+ if (payload.getClass().getName().equals("com.amazonaws.services.lambda.runtime.events.S3Event")) {
+ if (this.s3EventSerializer.get() == null) {
+ this.s3EventSerializer.set(new S3EventSerializer<>().withClassLoader(ClassUtils.getDefaultClassLoader()));
+ }
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ this.s3EventSerializer.get().toJson(payload, stream);
+ return stream.toByteArray();
+ }
+
+
return jsonMapper.toJson(payload);
}
diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java
index 917ca8693..0b55bc283 100644
--- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java
+++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java
@@ -28,10 +28,14 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import com.amazonaws.services.lambda.runtime.ClientContext;
+import com.amazonaws.services.lambda.runtime.CognitoIdentity;
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.LambdaLogger;
+import com.amazonaws.services.lambda.runtime.LambdaRuntime;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper;
import org.springframework.cloud.function.context.config.RoutingFunction;
@@ -130,6 +134,8 @@ private void eventLoop(ConfigurableApplicationContext context) {
logger.debug("Attempting to get new event");
ResponseEntity response = this.pollForData(rest, requestEntity);
+ Context clientContext = generateClientContext(response.getHeaders());
+
if (logger.isDebugEnabled()) {
logger.debug("New Event received: " + response);
}
@@ -140,9 +146,9 @@ private void eventLoop(ConfigurableApplicationContext context) {
FunctionInvocationWrapper function = locateFunction(environment, functionCatalog, response.getHeaders());
ByteArrayInputStream is = new ByteArrayInputStream(response.getBody().getBytes(StandardCharsets.UTF_8));
- Message> requestMessage = AWSLambdaUtils.generateMessage(is, function.getInputType(), function.isSupplier(), mapper, null);
-
+ Message> requestMessage = AWSLambdaUtils.generateMessage(is, function.getInputType(), function.isSupplier(), mapper, clientContext);
Object functionResponse = function.apply(requestMessage);
+
byte[] responseBytes = AWSLambdaUtils.generateOutputFromObject(requestMessage, functionResponse, mapper, function.getOutputType());
String invocationUrl = MessageFormat
@@ -157,12 +163,91 @@ private void eventLoop(ConfigurableApplicationContext context) {
}
}
catch (Exception e) {
+ e.printStackTrace();
this.propagateAwsError(requestId, e, mapper, runtimeApi, rest);
}
}
}
}
+ private Context generateClientContext(HttpHeaders headers) {
+
+ Map environment = System.getenv();
+
+ Context context = new Context() {
+
+ @Override
+ public int getRemainingTimeInMillis() {
+ long now = System.currentTimeMillis();
+ if (!headers.containsKey("Lambda-Runtime-Deadline-Ms")) {
+ return 0;
+ }
+ int delta = (int) (Long.parseLong(headers.getFirst("Lambda-Runtime-Deadline-Ms")) - now);
+ return delta > 0 ? delta : 0;
+ }
+
+ @Override
+ public int getMemoryLimitInMB() {
+ if (!environment.containsKey("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")) {
+ return 128;
+ }
+ return Integer.parseInt(environment.getOrDefault("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128"));
+ }
+
+ @Override
+ public LambdaLogger getLogger() {
+ return LambdaRuntime.getLogger();
+ }
+
+ @Override
+ public String getLogStreamName() {
+ return environment.get("LOG_STREAM_NAME");
+ }
+
+ @Override
+ public String getLogGroupName() {
+ return environment.get("LOG_GROUP_NAME");
+ }
+
+ @Override
+ public String getInvokedFunctionArn() {
+ return headers.getFirst("Lambda-Runtime-Invoked-Function-Arn");
+ }
+
+ @Override
+ public CognitoIdentity getIdentity() {
+ return null;
+ }
+
+ @Override
+ public String getFunctionVersion() {
+ return environment.get("FUNCTION_VERSION");
+ }
+
+ @Override
+ public String getFunctionName() {
+ return environment.get("FUNCTION_NAME");
+ }
+
+ @Override
+ public ClientContext getClientContext() {
+ return null;
+ }
+
+ @Override
+ public String getAwsRequestId() {
+ return headers.getFirst("Lambda-Runtime-Aws-Request-Id");
+ }
+
+ public String toString() {
+ return "FUNCTION NAME: " + getFunctionName() + ", FUNCTION VERSION: " + getFunctionVersion()
+ + ", FUNCTION ARN: " + getInvokedFunctionArn() + ", FUNCTION MEM LIMIT: " + getMemoryLimitInMB()
+ + ", FUNCTION DEADLINE: " + getRemainingTimeInMillis();
+ }
+ };
+ return context;
+ }
+
private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String runtimeApi, RestTemplate rest) {
String errorMessage = e.getMessage();
String errorType = e.getClass().getSimpleName();
diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java
index 93bfde19d..dfbc067c7 100644
--- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java
+++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java
@@ -85,6 +85,9 @@ public void handleRequest(InputStream input, OutputStream output, Context contex
if (!this.started) {
this.start();
}
+ if (context == null) {
+ logger.warn("Lambda is invoked with null Context");
+ }
Message requestMessage = AWSLambdaUtils
.generateMessage(input, this.function.getInputType(), this.function.isSupplier(), jsonMapper, context);
diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java
index 868c2ba01..817cbd776 100644
--- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java
+++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java
@@ -16,6 +16,7 @@
package org.springframework.cloud.function.adapter.aws;
+import java.util.Locale;
import java.util.function.Function;
import org.junit.jupiter.api.Test;
@@ -219,7 +220,7 @@ public void test_definitionLookupAndComposition() throws Exception {
protected static class SingleFunctionConfiguration {
@Bean
public Function uppercase() {
- return v -> v.toUpperCase();
+ return v -> v.toUpperCase(Locale.ROOT);
}
}
@@ -236,7 +237,7 @@ public Function, Flux> uppercase() {
protected static class MultipleFunctionConfiguration {
@Bean
public Function uppercase() {
- return v -> v.toUpperCase();
+ return v -> v.toUpperCase(Locale.ROOT);
}
@Bean
@@ -246,7 +247,7 @@ public Function toPersonJson() {
@Bean
public Function uppercasePerson() {
- return p -> new Person(p.getName().toUpperCase());
+ return p -> new Person(p.getName().toUpperCase(Locale.ROOT));
}
@Bean
@@ -267,7 +268,7 @@ public PersonFunction() {
@Override
public Person apply(Person input) {
- return new Person(input.getName().toUpperCase());
+ return new Person(input.getName().toUpperCase(Locale.ROOT));
}
}
diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java
index 3db5bb6e4..887e49f18 100644
--- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java
+++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java
@@ -25,6 +25,7 @@
import java.util.Base64;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -45,6 +46,7 @@
import com.amazonaws.services.lambda.runtime.events.SNSEvent;
import com.amazonaws.services.lambda.runtime.events.SQSEvent;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@@ -63,7 +65,6 @@
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.jupiter.api.Assertions.fail;
/**
*
@@ -998,6 +999,18 @@ public void testS3StringEvent() throws Exception {
assertThat(result).contains("s3SchemaVersion");
}
+ @Test
+ public void testS3EventAsOutput() throws Exception {
+ System.setProperty("MAIN_CLASS", S3Configuration.class.getName());
+ System.setProperty("spring.cloud.function.definition", "outputS3Event");
+ FunctionInvoker invoker = new FunctionInvoker();
+
+ InputStream targetStream = new ByteArrayInputStream(this.s3Event.getBytes());
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ invoker.handleRequest(targetStream, output, null);
+ assertThat(output.toByteArray()).isNotNull();
+ }
+
@Test
public void testS3Event() throws Exception {
System.setProperty("MAIN_CLASS", S3Configuration.class.getName());
@@ -1449,7 +1462,7 @@ public void testWithDefaultRoutingFailure() throws Exception {
try {
invoker.handleRequest(targetStream, output, null);
- fail();
+ Assertions.fail("");
}
catch (Exception e) {
// TODO: handle exception
@@ -1492,7 +1505,7 @@ public static class BasicConfiguration {
@Bean
public Function, Message> uppercase() {
return v -> {
- return MessageBuilder.withPayload(v.getPayload().toUpperCase()).build();
+ return MessageBuilder.withPayload(v.getPayload().toUpperCase(Locale.ROOT)).build();
};
}
}
@@ -1525,7 +1538,7 @@ public Function echoString() {
@Bean
public Function uppercase() {
- return v -> v.toUpperCase();
+ return v -> v.toUpperCase(Locale.ROOT);
}
@Bean
@@ -1678,6 +1691,13 @@ public Function