diff --git a/README.md b/README.md index 3f26e0c1b..b7da76ea2 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Currently the following versions are maintained: |---------|--------|-----------------------------|-----------------|------------------------|----------------|---------------| | 1.x | [1.x](https://github.com/aws/serverless-java-container/tree/1.x) | Java EE (javax.*) | 5.x (Boot 2.x) | 2.x | :white_check_mark: | :white_check_mark: | | 2.x | [main](https://github.com/aws/serverless-java-container/tree/main) | Jakarta EE 9-10 (jakarta.*) | 6.x (Boot 3.x) | 3.x | :x: | :x: | -| 3.x | | Jakarta EE 11 (jakarta.*) | 7.x (Boot 4.x) | 4.x | :x: | :x: | +| 3.x | [main](https://github.com/aws/serverless-java-container/tree/main) | Jakarta EE 11 (jakarta.*) | 7.x (Boot 4.x) | 4.x | :x: | :x: | Follow the quick start guides in [our wiki](https://github.com/aws/serverless-java-container/wiki) to integrate Serverless Java Container with your project: * [Spring quick start](https://github.com/aws/serverless-java-container/wiki/Quick-start---Spring) diff --git a/aws-serverless-java-container-core/pom.xml b/aws-serverless-java-container-core/pom.xml index e9aa6bbec..9929a6149 100644 --- a/aws-serverless-java-container-core/pom.xml +++ b/aws-serverless-java-container-core/pom.xml @@ -6,18 +6,19 @@ AWS Serverless Java container support - Core Allows Java applications written for a servlet container to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT .. 3.1.0 6.0.0 + 3.0.2 @@ -40,13 +41,13 @@ - com.fasterxml.jackson.core + tools.jackson.core jackson-databind ${jackson.version} - com.fasterxml.jackson.module + tools.jackson.module jackson-module-afterburner ${jackson.version} diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java index 0ae00b4da..8e13bd799 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandler.java @@ -18,7 +18,7 @@ import com.amazonaws.serverless.proxy.model.ErrorModel; import com.amazonaws.serverless.proxy.model.Headers; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import jakarta.ws.rs.core.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -103,7 +103,7 @@ protected String getErrorJson(String message) { try { return LambdaContainerHandler.getObjectMapper().writeValueAsString(new ErrorModel(message)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { log.error("Could not produce error JSON", e); return "{ \"message\": \"" + message + "\" }"; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java index ff978456b..86d4216a1 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java @@ -19,12 +19,10 @@ import com.amazonaws.serverless.proxy.model.ContainerConfig; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonParseException; -import com.fasterxml.jackson.databind.JsonMappingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectReader; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectReader; +import tools.jackson.databind.ObjectWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,10 +84,9 @@ public abstract class LambdaContainerHandler { return subject; }); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { log.error("Error while attempting to parse JWT body for requestId: " + SecurityUtils.crlf(event.getRequestContext().getRequestId()), e); return null; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java index 2cf6d77a6..8226be983 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/model/HttpApiV2AuthorizerMap.java @@ -13,17 +13,16 @@ package com.amazonaws.serverless.proxy.model; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.deser.std.StdDeserializer; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.databind.type.TypeFactory; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.deser.std.StdDeserializer; +import tools.jackson.databind.ser.std.StdSerializer; +import tools.jackson.databind.type.TypeFactory; import java.io.IOException; import java.util.HashMap; @@ -77,10 +76,9 @@ public HttpApiV2AuthorizerDeserializer() { } @Override - public HttpApiV2AuthorizerMap deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException, JsonProcessingException { + public HttpApiV2AuthorizerMap deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) { HttpApiV2AuthorizerMap map = new HttpApiV2AuthorizerMap(); - JsonNode node = jsonParser.getCodec().readTree(jsonParser); + JsonNode node = deserializationContext.readTree(jsonParser); if (node.has(JWT_KEY)) { HttpApiV2JwtAuthorizer authorizer = LambdaContainerHandler.getObjectMapper() .treeToValue(node.get(JWT_KEY), HttpApiV2JwtAuthorizer.class); @@ -88,7 +86,7 @@ public HttpApiV2AuthorizerMap deserialize(JsonParser jsonParser, Deserialization } if (node.has(LAMBDA_KEY)) { Map context = LambdaContainerHandler.getObjectMapper().treeToValue(node.get(LAMBDA_KEY), - TypeFactory.defaultInstance().constructMapType(HashMap.class, String.class, Object.class)); + LambdaContainerHandler.getObjectMapper().getTypeFactory().constructMapType(HashMap.class, String.class, Object.class)); map.put(LAMBDA_KEY, context); } if (node.has(IAM_KEY)) { @@ -110,16 +108,19 @@ public HttpApiV2AuthorizerSerializer() { @Override public void serialize(HttpApiV2AuthorizerMap httpApiV2AuthorizerMap, JsonGenerator jsonGenerator, - SerializerProvider serializerProvider) throws IOException { + SerializationContext serializationContext) { jsonGenerator.writeStartObject(); if (httpApiV2AuthorizerMap.isJwt()) { - jsonGenerator.writeObjectField(JWT_KEY, httpApiV2AuthorizerMap.getJwtAuthorizer()); + jsonGenerator.writeName(JWT_KEY); + jsonGenerator.writePOJO(httpApiV2AuthorizerMap.getJwtAuthorizer()); } if (httpApiV2AuthorizerMap.isLambda()) { - jsonGenerator.writeObjectField(LAMBDA_KEY, httpApiV2AuthorizerMap.getLambdaAuthorizerContext()); + jsonGenerator.writeName(LAMBDA_KEY); + jsonGenerator.writePOJO(httpApiV2AuthorizerMap.getLambdaAuthorizerContext()); } if (httpApiV2AuthorizerMap.isIam()) { - jsonGenerator.writeObjectField(IAM_KEY, httpApiV2AuthorizerMap.get(IAM_KEY)); + jsonGenerator.writeName(IAM_KEY); + jsonGenerator.writePOJO(httpApiV2AuthorizerMap.get(IAM_KEY)); } jsonGenerator.writeEndObject(); } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java index 012827e40..e15b9876f 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/AwsProxyExceptionHandlerTest.java @@ -5,8 +5,8 @@ import com.amazonaws.serverless.exceptions.InvalidResponseObjectException; import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.ErrorModel; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; @@ -47,7 +47,7 @@ void typedHandle_InvalidRequestEventException_500State() { @Test void typedHandle_InvalidRequestEventException_responseString() - throws JsonProcessingException { + throws JacksonException { AwsProxyResponse resp = exceptionHandler.handle(new InvalidRequestEventException(INVALID_REQUEST_MESSAGE, null)); assertNotNull(resp); @@ -74,7 +74,7 @@ void typedHandle_InvalidResponseObjectException_502State() { @Test void typedHandle_InvalidResponseObjectException_responseString() - throws JsonProcessingException { + throws JacksonException { AwsProxyResponse resp = exceptionHandler.handle(new InvalidResponseObjectException(INVALID_RESPONSE_MESSAGE, null)); assertNotNull(resp); @@ -106,7 +106,7 @@ void typedHandle_InternalServerErrorException_500State() { @Test void typedHandle_InternalServerErrorException_responseString() - throws JsonProcessingException { + throws JacksonException { InternalServerErrorException mockInternalServerErrorException = Mockito.mock(InternalServerErrorException.class); Mockito.when(mockInternalServerErrorException.getMessage()).thenReturn(INTERNAL_SERVER_ERROR_MESSAGE); @@ -131,7 +131,7 @@ void typedHandle_InternalServerErrorException_jsonContentTypeHeader() { @Test void typedHandle_NullPointerException_responseObject() - throws JsonProcessingException { + throws JacksonException { AwsProxyResponse resp = exceptionHandler.handle(new NullPointerException()); assertNotNull(resp); @@ -248,7 +248,7 @@ void getErrorJson_ErrorModel_validJson() void getErrorJson_JsonParsinException_validJson() throws IOException { ObjectMapper mockMapper = mock(ObjectMapper.class); - JsonProcessingException exception = mock(JsonProcessingException.class); + JacksonException exception = mock(JacksonException.class); when(mockMapper.writeValueAsString(any(Object.class))).thenThrow(exception); String output = exceptionHandler.getErrorJson(INVALID_RESPONSE_MESSAGE); diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java index e42130453..a817bd2bf 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/internal/testutils/AwsProxyRequestBuilder.java @@ -15,8 +15,8 @@ import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; import com.amazonaws.serverless.proxy.model.*; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.commons.io.IOUtils; import org.apache.hc.core5.http.ContentType; @@ -106,7 +106,7 @@ public AwsProxyRequestBuilder alb() { try { String json = objectMapper.writeValueAsString(this.request); albRequest = objectMapper.readValue(json, AwsProxyRequest.class); - } catch (JsonProcessingException jpe) { + } catch (JacksonException jpe) { throw new RuntimeException(jpe); } @@ -265,7 +265,7 @@ public AwsProxyRequestBuilder body(Object body) { if (request.getMultiValueHeaders() != null && request.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)) { try { return body(LambdaContainerHandler.getObjectMapper().writeValueAsString(body)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { throw new UnsupportedOperationException("Could not serialize object: " + e.getMessage()); } } else { @@ -438,7 +438,7 @@ public InputStream buildStream() { try { String requestJson = LambdaContainerHandler.getObjectMapper().writeValueAsString(request); return new ByteArrayInputStream(requestJson.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { return null; } } @@ -448,7 +448,7 @@ public InputStream toHttpApiV2RequestStream() { try { String requestJson = LambdaContainerHandler.getObjectMapper().writeValueAsString(req); return new ByteArrayInputStream(requestJson.getBytes(StandardCharsets.UTF_8)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { return null; } } diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java index 6d0aa9eeb..07bdff979 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/AwsProxyRequestTest.java @@ -7,7 +7,7 @@ import java.io.IOException; import org.junit.jupiter.api.Test; import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; public class AwsProxyRequestTest { private static final String CUSTOM_HEADER_KEY_LOWER_CASE = "custom-header"; diff --git a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java index 20ff4dff2..e51a1602b 100644 --- a/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java +++ b/aws-serverless-java-container-core/src/test/java/com/amazonaws/serverless/proxy/model/HttpApiV2ProxyRequestTest.java @@ -1,7 +1,7 @@ package com.amazonaws.serverless.proxy.model; import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; -import com.fasterxml.jackson.core.JsonProcessingException; +import tools.jackson.core.JacksonException; import org.junit.jupiter.api.Test; import java.util.ArrayList; @@ -181,7 +181,7 @@ void deserialize_fromJsonString_authorizerPopulatedCorrectly() { assertTrue(req.getRequestContext().getAuthorizer().getJwtAuthorizer().getClaims().containsKey("claim1")); assertEquals(2, req.getRequestContext().getAuthorizer().getJwtAuthorizer().getScopes().size()); assertEquals(RequestSource.API_GATEWAY, req.getRequestSource()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -196,7 +196,7 @@ void deserialize_fromJsonString_authorizerEmptyMap() { assertFalse(req.getRequestContext().getAuthorizer().isJwt()); assertFalse(req.getRequestContext().getAuthorizer().isLambda()); assertFalse(req.getRequestContext().getAuthorizer().isIam()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -212,7 +212,7 @@ void deserialize_fromJsonString_lambdaAuthorizer() { assertTrue(req.getRequestContext().getAuthorizer().isLambda()); assertEquals(5, req.getRequestContext().getAuthorizer().getLambdaAuthorizerContext().size()); assertEquals(1, req.getRequestContext().getAuthorizer().getLambdaAuthorizerContext().get("numberKey")); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -239,7 +239,7 @@ void deserialize_fromJsonString_iamAuthorizer() { req.getRequestContext().getAuthorizer().getIamAuthorizer().getUserArn()); assertEquals("AIDACOSFODNN7EXAMPLE2", req.getRequestContext().getAuthorizer().getIamAuthorizer().getUserId()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -254,7 +254,7 @@ void deserialize_fromJsonString_isBase64EncodedPopulates() { req = LambdaContainerHandler.getObjectMapper().readValue(NO_AUTH_PROXY, HttpApiV2ProxyRequest.class); assertTrue(req.isBase64Encoded()); assertEquals(RequestSource.API_GATEWAY, req.getRequestSource()); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing request" + e.getMessage()); } @@ -277,7 +277,7 @@ void serialize_toJsonString_authorizerPopulatesCorrectly() { assertTrue(reqString.contains("\"scopes\":[\"first\",\"second\"]")); assertTrue(reqString.contains("\"authorizer\":{\"jwt\":{")); assertTrue(reqString.contains("\"isBase64Encoded\":false")); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while serializing request" + e.getMessage()); } diff --git a/aws-serverless-java-container-jersey/pom.xml b/aws-serverless-java-container-jersey/pom.xml index 1788a36b8..8fcc07c4b 100644 --- a/aws-serverless-java-container-jersey/pom.xml +++ b/aws-serverless-java-container-jersey/pom.xml @@ -6,12 +6,12 @@ AWS Serverless Java container support - Jersey implementation Allows Java applications written for Jersey to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT .. @@ -24,18 +24,12 @@ com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT - - - com.fasterxml.jackson.core - jackson-databind - - + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT tests test-jar test @@ -68,14 +62,6 @@ test - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - true - test - - org.glassfish.jersey.media jersey-media-json-jackson @@ -88,11 +74,11 @@ jackson-annotations - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - com.fasterxml.jackson.core + tools.jackson.core jackson-core diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java index b845a0b69..23f1078fa 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyTest.java @@ -24,8 +24,8 @@ import com.amazonaws.serverless.proxy.model.AwsProxyResponse; import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import org.apache.commons.codec.binary.Base64; import org.glassfish.jersey.logging.LoggingFeature; import org.glassfish.jersey.media.multipart.MultiPartFeature; @@ -303,7 +303,7 @@ void error_statusCode_methodNotAllowed(String reqType) { @MethodSource("data") @ParameterizedTest - void responseBody_responseWriter_validBody(String reqType) throws JsonProcessingException { + void responseBody_responseWriter_validBody(String reqType) throws JacksonException { initJerseyAwsProxyTest(reqType); SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(CUSTOM_HEADER_VALUE); @@ -460,7 +460,7 @@ private void validateMapResponseModel(AwsProxyResponse output, String key, Strin MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); assertNotNull(response.getValues().get(key)); assertEquals(value, response.getValues().get(key)); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } @@ -471,7 +471,7 @@ private void validateSingleValueModel(AwsProxyResponse output, String value) { SingleValueModel response = objectMapper.readValue(output.getBody(), SingleValueModel.class); assertNotNull(response.getValue()); assertEquals(value, response.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java index 9dc1ab32a..5ca09b11b 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/jersey/JerseyParamEncodingTest.java @@ -11,7 +11,8 @@ import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; import org.junit.jupiter.api.Disabled; @@ -281,7 +282,7 @@ private void validateSingleValueModel(AwsProxyResponse output, String value) { SingleValueModel response = objectMapper.readValue(output.getBody(), SingleValueModel.class); assertNotNull(response.getValue()); assertEquals(value, response.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } @@ -292,7 +293,7 @@ private void validateMapResponseModel(AwsProxyResponse output, String key, Strin MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); assertNotNull(response.getValues().get(key)); assertEquals(value, response.getValues().get(key)); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } diff --git a/aws-serverless-java-container-spring/pom.xml b/aws-serverless-java-container-spring/pom.xml index f03d9db0a..d85534faf 100644 --- a/aws-serverless-java-container-spring/pom.xml +++ b/aws-serverless-java-container-spring/pom.xml @@ -6,18 +6,18 @@ AWS Serverless Java container support - Spring implementation Allows Java applications written for the Spring framework to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT .. - 6.2.8 - 6.5.1 + 7.0.0 + 7.0.0 @@ -25,12 +25,12 @@ com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT tests test-jar test @@ -57,12 +57,7 @@ test - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - test - + jakarta.activation diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java index 9504b6166..4b7d3eb81 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java @@ -15,8 +15,8 @@ import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; import com.amazonaws.serverless.proxy.spring.echoapp.model.SingleValueModel; import com.amazonaws.services.lambda.runtime.Context; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.ObjectMapper; import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; @@ -294,7 +294,7 @@ void error_unauthenticatedCall_filterStepsRequest(String reqType) { @MethodSource("data") @ParameterizedTest - void responseBody_responseWriter_validBody(String reqType) throws JsonProcessingException { + void responseBody_responseWriter_validBody(String reqType) throws JacksonException { initSpringAwsProxyTest(reqType); SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(CUSTOM_HEADER_VALUE); @@ -311,7 +311,7 @@ void responseBody_responseWriter_validBody(String reqType) throws JsonProcessing @MethodSource("data") @ParameterizedTest - void responseBody_responseWriter_validBody_UTF(String reqType) throws JsonProcessingException { + void responseBody_responseWriter_validBody_UTF(String reqType) throws JacksonException { initSpringAwsProxyTest(reqType); SingleValueModel singleValueModel = new SingleValueModel(); singleValueModel.setValue(UNICODE_VALUE); @@ -363,7 +363,7 @@ void injectBody_populatedResponse_noException(String reqType) { try { SingleValueModel output = objectMapper.readValue(response.getBody(), SingleValueModel.class); assertEquals("true", output.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail(); } @@ -373,7 +373,7 @@ void injectBody_populatedResponse_noException(String reqType) { try { SingleValueModel output = objectMapper.readValue(emptyResp.getBody(), SingleValueModel.class); assertNull(output.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail(); } @@ -392,7 +392,7 @@ void servletRequestEncoding_acceptEncoding_okStatusCode(String reqType) { .header(HttpHeaders.ACCEPT_ENCODING, "gzip, deflate") .queryString("status", "200") .body(objectMapper.writeValueAsString(singleValueModel)); - } catch (JsonProcessingException e) { + } catch (JacksonException e) { fail("Could not serialize object to JSON"); } @@ -484,7 +484,7 @@ private void validateMapResponseModel(AwsProxyResponse output) { MapResponseModel response = objectMapper.readValue(output.getBody(), MapResponseModel.class); assertNotNull(response.getValues().get(CUSTOM_HEADER_KEY)); assertEquals(CUSTOM_HEADER_VALUE, response.getValues().get(CUSTOM_HEADER_KEY)); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } @@ -495,7 +495,7 @@ private void validateSingleValueModel(AwsProxyResponse output, String value) { SingleValueModel response = objectMapper.readValue(output.getBody(), SingleValueModel.class); assertNotNull(response.getValue()); assertEquals(value, response.getValue()); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Exception while parsing response body: " + e.getMessage()); } diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java index cf4aa495e..9b3e0cecc 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java @@ -1,7 +1,7 @@ package com.amazonaws.serverless.proxy.spring.echoapp; import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java index 7742db7f6..d08a3e45c 100644 --- a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/profile/SpringProfileTest.java @@ -8,7 +8,7 @@ import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; import com.amazonaws.serverless.proxy.spring.echoapp.EchoSpringAppConfig; import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; -import com.fasterxml.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; diff --git a/aws-serverless-java-container-springboot3/pom.xml b/aws-serverless-java-container-springboot3/pom.xml index dc9ffca03..b636dd61d 100644 --- a/aws-serverless-java-container-springboot3/pom.xml +++ b/aws-serverless-java-container-springboot3/pom.xml @@ -3,7 +3,7 @@ aws-serverless-java-container com.amazonaws.serverless - 2.1.5-SNAPSHOT + 2.1.5 4.0.0 @@ -12,7 +12,6 @@ AWS Serverless Java container support - SpringBoot 3 implementation Allows Java applications written for SpringBoot 3 to run in AWS Lambda https://aws.amazon.com/lambda - 2.1.5-SNAPSHOT 6.2.8 @@ -30,16 +29,22 @@ com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 2.1.5 com.amazonaws.serverless aws-serverless-java-container-core - 2.1.5-SNAPSHOT + 2.1.5 tests test-jar test + + com.github.spotbugs + spotbugs-annotations + 4.9.3 + provided + org.springframework spring-webflux diff --git a/aws-serverless-java-container-springboot4/pom.xml b/aws-serverless-java-container-springboot4/pom.xml new file mode 100644 index 000000000..a897a65a5 --- /dev/null +++ b/aws-serverless-java-container-springboot4/pom.xml @@ -0,0 +1,93 @@ + + + + aws-serverless-java-container + com.amazonaws.serverless + 3.0.0-SNAPSHOT + + 4.0.0 + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + AWS Serverless Java container support - SpringBoot 4 implementation + Allows Java applications written for SpringBoot 4 to run in AWS Lambda + https://aws.amazon.com/lambda + 3.0.0-SNAPSHOT + + + 7.0.0 + 4.0.0 + 7.0.0 + 5.0.0 + + + + + org.springframework.cloud + spring-cloud-function-serverless-web + ${springcloud.function.version} + + + com.amazonaws.serverless + aws-serverless-java-container-core + 3.0.0-SNAPSHOT + + + com.amazonaws.serverless + aws-serverless-java-container-core + 3.0.0-SNAPSHOT + tests + test-jar + test + + + org.springframework + spring-webflux + ${spring.version} + true + + + org.springframework.boot + spring-boot + ${springboot.version} + true + + + org.springframework.boot + spring-boot-autoconfigure + ${springboot.version} + true + + + org.springframework + spring-core + ${spring.version} + true + + + org.springframework + spring-context + ${spring.version} + true + + + org.springframework + spring-webmvc + ${spring.version} + true + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 25 + 25 + + + + + diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor.java new file mode 100644 index 000000000..0a461aa97 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringAotTypesProcessor.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024-2024 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 com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.model.*; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import tools.jackson.core.JsonToken; + +/** + * AOT Initialization processor required to register reflective hints for GraalVM. + * This is necessary to ensure proper JSON serialization/deserialization. + * It is registered with META-INF/spring/aot.factories + * + * @author Oleg Zhurakousky + */ +public class AwsSpringAotTypesProcessor implements BeanFactoryInitializationAotProcessor { + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + return new ReflectiveProcessorBeanFactoryInitializationAotContribution(); + } + + private static final class ReflectiveProcessorBeanFactoryInitializationAotContribution implements BeanFactoryInitializationAotContribution { + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + RuntimeHints runtimeHints = generationContext.getRuntimeHints(); + // known static types + + runtimeHints.reflection().registerType(AwsProxyRequest.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(AwsProxyResponse.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(SingleValueHeaders.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(JsonToken.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(MultiValuedTreeMap.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(Headers.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(AwsProxyRequestContext.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(ApiGatewayRequestIdentity.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES); + runtimeHints.reflection().registerType(AwsHttpServletResponse.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2ProxyRequest.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2HttpContext.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2ProxyRequestContext.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2AuthorizerMap.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2AuthorizerMap.HttpApiV2AuthorizerDeserializer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2AuthorizerMap.HttpApiV2AuthorizerSerializer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2IamAuthorizer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(HttpApiV2JwtAuthorizer.class, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, + MemberCategory.DECLARED_FIELDS, MemberCategory.DECLARED_CLASSES, MemberCategory.INTROSPECT_DECLARED_METHODS); + } + + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java new file mode 100644 index 000000000..7c262a072 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtils.java @@ -0,0 +1,222 @@ +package com.amazonaws.serverless.proxy.spring; + +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import com.amazonaws.serverless.proxy.internal.HttpUtils; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.model.RequestSource; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.cloud.function.serverless.web.ServerlessHttpServletRequest; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.util.CollectionUtils; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.MultiValueMapAdapter; +import org.springframework.util.StringUtils; + +import com.amazonaws.serverless.proxy.AwsHttpApiV2SecurityContextWriter; +import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.SecurityContextWriter; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.services.lambda.runtime.Context; +import tools.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; + +import static com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest.decodeValueIfEncoded; +import static com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest.getQueryParamValuesAsList; + +class AwsSpringHttpProcessingUtils { + + private static Log logger = LogFactory.getLog(AwsSpringHttpProcessingUtils.class); + private static final int LAMBDA_MAX_REQUEST_DURATION_MINUTES = 15; + + private AwsSpringHttpProcessingUtils() { + + } + + public static AwsProxyResponse processRequest(HttpServletRequest request, ServerlessMVC mvc, + AwsProxyHttpServletResponseWriter responseWriter) { + CountDownLatch latch = new CountDownLatch(1); + AwsHttpServletResponse response = new AwsHttpServletResponse(request, latch); + try { + mvc.service(request, response); + boolean requestTimedOut = !latch.await(LAMBDA_MAX_REQUEST_DURATION_MINUTES, TimeUnit.MINUTES); // timeout is potentially lower as user configures it + if (requestTimedOut) { + logger.warn("request timed out after " + LAMBDA_MAX_REQUEST_DURATION_MINUTES + " minutes"); + } + AwsProxyResponse awsResponse = responseWriter.writeResponse(response, null); + return awsResponse; + } + catch (Exception e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + } + + public static String extractVersion() { + try { + String path = AwsSpringHttpProcessingUtils.class.getProtectionDomain().getCodeSource().getLocation().toString(); + int endIndex = path.lastIndexOf('.'); + if (endIndex < 0) { + return "UNKNOWN-VERSION"; + } + int startIndex = path.lastIndexOf("/") + 1; + return path.substring(startIndex, endIndex).replace("spring-cloud-function-serverless-web-", ""); + } + catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to detect version", e); + } + return "UNKNOWN-VERSION"; + } + + } + + public static HttpServletRequest generateHttpServletRequest(InputStream jsonRequest, Context lambdaContext, + ServletContext servletContext, ObjectMapper mapper) { + try { + String text = new String(FileCopyUtils.copyToByteArray(jsonRequest), StandardCharsets.UTF_8); + if (logger.isDebugEnabled()) { + logger.debug("Creating HttpServletRequest from: " + text); + } + return generateHttpServletRequest(text, lambdaContext, servletContext, mapper); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static HttpServletRequest generateHttpServletRequest(String jsonRequest, Context lambdaContext, + ServletContext servletContext, ObjectMapper mapper) { + Map _request = readValue(jsonRequest, Map.class, mapper); + SecurityContextWriter securityWriter = "2.0".equals(_request.get("version")) + ? new AwsHttpApiV2SecurityContextWriter() + : new AwsProxySecurityContextWriter(); + HttpServletRequest httpServletRequest = "2.0".equals(_request.get("version")) + ? AwsSpringHttpProcessingUtils.generateRequest2(jsonRequest, lambdaContext, securityWriter, mapper, servletContext) + : AwsSpringHttpProcessingUtils.generateRequest1(jsonRequest, lambdaContext, securityWriter, mapper, servletContext); + return httpServletRequest; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static HttpServletRequest generateRequest1(String request, Context lambdaContext, + SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) { + AwsProxyRequest v1Request = readValue(request, AwsProxyRequest.class, mapper); + + // Use AWS container's servlet request instead of Spring Cloud Function's + return new AwsProxyHttpServletRequest(v1Request, lambdaContext, securityWriter.writeSecurityContext(v1Request, lambdaContext)); + } + + + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static HttpServletRequest generateRequest2(String request, Context lambdaContext, + SecurityContextWriter securityWriter, ObjectMapper mapper, ServletContext servletContext) { + HttpApiV2ProxyRequest v2Request = readValue(request, HttpApiV2ProxyRequest.class, mapper); + + + ServerlessHttpServletRequest httpRequest = new ServerlessHttpServletRequest(servletContext, + v2Request.getRequestContext().getHttp().getMethod(), v2Request.getRequestContext().getHttp().getPath()); + populateQueryStringParametersV2(v2Request.getQueryStringParameters(), httpRequest); + + v2Request.getHeaders().forEach(httpRequest::setHeader); + + populateContentAndContentType( + v2Request.getBody(), + v2Request.getHeaders().get(HttpHeaders.CONTENT_TYPE), + v2Request.isBase64Encoded(), + httpRequest + ); + + httpRequest.setAttribute(RequestReader.HTTP_API_CONTEXT_PROPERTY, v2Request.getRequestContext()); + httpRequest.setAttribute(RequestReader.HTTP_API_STAGE_VARS_PROPERTY, v2Request.getStageVariables()); + httpRequest.setAttribute(RequestReader.HTTP_API_EVENT_PROPERTY, v2Request); + httpRequest.setAttribute(RequestReader.LAMBDA_CONTEXT_PROPERTY, lambdaContext); + httpRequest.setAttribute(RequestReader.JAX_SECURITY_CONTEXT_PROPERTY, + securityWriter.writeSecurityContext(v2Request, lambdaContext)); + return httpRequest; + } + + private static void populateQueryStringParametersV2(Map requestParameters, ServerlessHttpServletRequest httpRequest) { + if (!CollectionUtils.isEmpty(requestParameters)) { + for (Entry entry : requestParameters.entrySet()) { + // fix according to parseRawQueryString + httpRequest.setParameter(entry.getKey(), entry.getValue()); + } + } + } + + private static void populateQueryStringParametersV1(AwsProxyRequest v1Request, ServerlessHttpServletRequest httpRequest) { + Map requestParameters = v1Request.getQueryStringParameters(); + if (!CollectionUtils.isEmpty(requestParameters)) { + // decode all keys and values in map + for (Entry entry : requestParameters.entrySet()) { + String k = v1Request.getRequestSource() == RequestSource.ALB ? decodeValueIfEncoded(entry.getKey()) : entry.getKey(); + String v = v1Request.getRequestSource() == RequestSource.ALB ? decodeValueIfEncoded(entry.getValue()) : entry.getValue(); + httpRequest.setParameter(k, v); + } + } + } + + private static void populateMultiValueQueryStringParametersV1(AwsProxyRequest v1Request, ServerlessHttpServletRequest httpRequest) { + if (v1Request.getMultiValueQueryStringParameters() != null) { + MultiValueMapAdapter queryStringParameters = new MultiValueMapAdapter<>(v1Request.getMultiValueQueryStringParameters()); + queryStringParameters.forEach((k, v) -> { + String key = v1Request.getRequestSource() == RequestSource.ALB + ? decodeValueIfEncoded(k) + : k; + List value = v1Request.getRequestSource() == RequestSource.ALB + ? getQueryParamValuesAsList(v1Request.getMultiValueQueryStringParameters(), k, false).stream() + .map(AwsHttpServletRequest::decodeValueIfEncoded) + .toList() + : v; + httpRequest.setParameter(key, value.toArray(new String[0])); + }); + } + } + + private static T readValue(String json, Class clazz, ObjectMapper mapper) { + try { + return mapper.readValue(json, clazz); + } + catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private static void populateContentAndContentType( + String body, + String contentType, + boolean base64Encoded, + ServerlessHttpServletRequest httpRequest) { + if (StringUtils.hasText(body)) { + httpRequest.setContentType(contentType == null ? MediaType.APPLICATION_JSON_VALUE : contentType); + if (base64Encoded) { + httpRequest.setContent(Base64.getMimeDecoder().decode(body)); + } else { + Charset charseEncoding = HttpUtils.parseCharacterEncoding(contentType,StandardCharsets.UTF_8); + httpRequest.setContent(body.getBytes(charseEncoding)); + } + } + } + + + +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop.java new file mode 100644 index 000000000..f9c316185 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebCustomRuntimeEventLoop.java @@ -0,0 +1,184 @@ +/* + * Copyright 2024-2024 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 com.amazonaws.serverless.proxy.spring; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URI; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; +import org.springframework.context.SmartLifecycle; +import org.springframework.core.env.Environment; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationFeature; +import tools.jackson.databind.json.JsonMapper; + +/** + * Event loop and necessary configurations to support AWS Lambda Custom Runtime + * - https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html. + * + * @author Oleg Zhurakousky + * @author Mark Sailes + * + */ +public final class AwsSpringWebCustomRuntimeEventLoop implements SmartLifecycle { + + private static Log logger = LogFactory.getLog(AwsSpringWebCustomRuntimeEventLoop.class); + + static final String LAMBDA_VERSION_DATE = "2018-06-01"; + private static final String LAMBDA_ERROR_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/error"; + private static final String LAMBDA_RUNTIME_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/next"; + private static final String LAMBDA_INVOCATION_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/response"; + private static final String USER_AGENT_VALUE = String.format("spring-cloud-function/%s-%s", + System.getProperty("java.runtime.version"), AwsSpringHttpProcessingUtils.extractVersion()); + + private final ServletWebServerApplicationContext applicationContext; + + private volatile boolean running; + + private final ExecutorService executor = Executors.newSingleThreadExecutor(); + + public AwsSpringWebCustomRuntimeEventLoop(ServletWebServerApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + public void run() { + this.running = true; + this.executor.execute(() -> { + eventLoop(this.applicationContext); + }); + } + + @Override + public void start() { + this.run(); + } + + @Override + public void stop() { + this.executor.shutdownNow(); + this.running = false; + } + + @Override + public boolean isRunning() { + return this.running; + } + + private void eventLoop(ServletWebServerApplicationContext context) { + ServerlessMVC mvc = ServerlessMVC.INSTANCE(context); + + Environment environment = context.getEnvironment(); + logger.info("Starting AWSWebRuntimeEventLoop"); + + String runtimeApi = environment.getProperty("AWS_LAMBDA_RUNTIME_API"); + String eventUri = MessageFormat.format(LAMBDA_RUNTIME_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE); + if (logger.isDebugEnabled()) { + logger.debug("Event URI: " + eventUri); + } + + RequestEntity requestEntity = RequestEntity.get(URI.create(eventUri)) + .header("User-Agent", USER_AGENT_VALUE).build(); + RestTemplate rest = new RestTemplate(); + ObjectMapper mapper = JsonMapper.builder() + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) + .build(); + AwsProxyHttpServletResponseWriter responseWriter = new AwsProxyHttpServletResponseWriter(); + + logger.info("Entering event loop"); + while (this.isRunning()) { + logger.debug("Attempting to get new event"); + ResponseEntity incomingEvent = rest.exchange(requestEntity, String.class); + + if (incomingEvent != null && incomingEvent.hasBody()) { + if (logger.isDebugEnabled()) { + logger.debug("New Event received from AWS Gateway: " + incomingEvent.getBody()); + } + String requestId = incomingEvent.getHeaders().getFirst("Lambda-Runtime-Aws-Request-Id"); + + try { + logger.debug("Submitting request to the user's web application"); + + AwsProxyResponse awsResponse = AwsSpringHttpProcessingUtils.processRequest( + AwsSpringHttpProcessingUtils.generateHttpServletRequest(incomingEvent.getBody(), + null, mvc.getServletContext(), mapper), mvc, responseWriter); + if (logger.isDebugEnabled()) { + logger.debug("Received response - body: " + awsResponse.getBody() + + "; status: " + awsResponse.getStatusCode() + "; headers: " + awsResponse.getHeaders()); + } + + String invocationUrl = MessageFormat.format(LAMBDA_INVOCATION_URL_TEMPLATE, runtimeApi, + LAMBDA_VERSION_DATE, requestId); + + ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(invocationUrl)) + .header("User-Agent", USER_AGENT_VALUE).body(awsResponse), byte[].class); + if (logger.isDebugEnabled()) { + logger.debug("Response sent: body: " + result.getBody() + + "; status: " + result.getStatusCode() + "; headers: " + result.getHeaders()); + } + if (logger.isInfoEnabled()) { + logger.info("Result POST status: " + result); + } + } + catch (Exception e) { + logger.error(e); + this.propagateAwsError(requestId, e, mapper, runtimeApi, rest); + } + } + } + } + + private void propagateAwsError(String requestId, Exception e, ObjectMapper mapper, String runtimeApi, RestTemplate rest) { + String errorMessage = e.getMessage(); + String errorType = e.getClass().getSimpleName(); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String stackTrace = sw.toString(); + Map em = new HashMap<>(); + em.put("errorMessage", errorMessage); + em.put("errorType", errorType); + em.put("stackTrace", stackTrace); + try { + byte[] outputBody = mapper.writeValueAsBytes(em); + String errorUrl = MessageFormat.format(LAMBDA_ERROR_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId); + ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(errorUrl)) + .header("User-Agent", USER_AGENT_VALUE) + .body(outputBody), Object.class); + if (logger.isInfoEnabled()) { + logger.info("Result ERROR status: " + result.getStatusCode()); + } + } + catch (Exception e2) { + throw new IllegalArgumentException("Failed to report error", e2); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebRuntimeInitializer.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebRuntimeInitializer.java new file mode 100644 index 000000000..9324ebdf0 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/AwsSpringWebRuntimeInitializer.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024-2024 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 com.amazonaws.serverless.proxy.spring; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.env.Environment; +import org.springframework.util.StringUtils; + +/** + * Initializer to optionally start Custom Runtime to process web workloads. + * Registered with META-INF/spring.factories + * + * @author Dave Syer + * @author Oleg Zhurakousky + */ +public class AwsSpringWebRuntimeInitializer implements ApplicationContextInitializer { + + private static Log logger = LogFactory.getLog(AwsSpringWebRuntimeInitializer.class); + + @Override + public void initialize(GenericApplicationContext context) { + Environment environment = context.getEnvironment(); + + if (context instanceof ServletWebServerApplicationContext && isCustomRuntime(environment)) { + if (context.getBeanFactory().getBeanNamesForType(AwsSpringWebCustomRuntimeEventLoop.class, false, false).length == 0) { + context.registerBean(StringUtils.uncapitalize(AwsSpringWebCustomRuntimeEventLoop.class.getSimpleName()), + SmartLifecycle.class, () -> new AwsSpringWebCustomRuntimeEventLoop((ServletWebServerApplicationContext) context)); + } + } + } + + private boolean isCustomRuntime(Environment environment) { + String handler = environment.getProperty("_HANDLER"); + if (StringUtils.hasText(handler)) { + handler = handler.split(":")[0]; + logger.info("AWS Handler: " + handler); + try { + Thread.currentThread().getContextClassLoader().loadClass(handler); + } + catch (Exception e) { + logger.debug("Will execute Lambda in Custom Runtime"); + return true; + } + } + return false; + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootAwsProxyExceptionHandler.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootAwsProxyExceptionHandler.java new file mode 100644 index 000000000..127ef6684 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootAwsProxyExceptionHandler.java @@ -0,0 +1,27 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; +import com.amazonaws.serverless.proxy.ExceptionHandler; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import org.springframework.web.ErrorResponse; + +/** + * This ExceptionHandler implementation enhances the standard AwsProxyExceptionHandler + * by mapping additional details from org.springframework.web.ErrorResponse + * + * As of now this class is identical with SpringAwsProxyExceptionHandler. We may consider + * moving it to a common module to share it in the future. + */ +public class SpringBootAwsProxyExceptionHandler extends AwsProxyExceptionHandler + implements ExceptionHandler { + @Override + public AwsProxyResponse handle(Throwable ex) { + if (ex instanceof ErrorResponse) { + return new AwsProxyResponse(((ErrorResponse) ex).getStatusCode().value(), + HEADERS, getErrorJson(ex.getMessage())); + } else { + return super.handle(ex); + } + } + +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java new file mode 100644 index 000000000..9ab22a8ea --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootLambdaContainerHandler.java @@ -0,0 +1,230 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring; + +import java.util.concurrent.CountDownLatch; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.context.servlet.AnnotationConfigServletWebApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.ExceptionHandler; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.ResponseWriter; +import com.amazonaws.serverless.proxy.SecurityContextWriter; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; +import com.amazonaws.serverless.proxy.internal.servlet.AwsServletContext; +import com.amazonaws.serverless.proxy.internal.servlet.AwsServletRegistration; +import com.amazonaws.serverless.proxy.internal.testutils.Timer; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.embedded.ServerlessReactiveServletEmbeddedServerFactory; +import com.amazonaws.serverless.proxy.spring.embedded.ServerlessServletEmbeddedServerFactory; +import com.amazonaws.services.lambda.runtime.Context; + +import jakarta.servlet.Servlet; +import jakarta.servlet.http.HttpServletRequest; + +/** + * SpringBoot implementation of the `LambdaContainerHandler` abstract class. This class uses the `LambdaSpringApplicationInitializer` + * object behind the scenes to proxy requests. The default implementation leverages the `AwsProxyHttpServletRequest` and + * `AwsHttpServletResponse` implemented in the `aws-serverless-java-container-core` package. + * + * Important: Make sure to add LambdaFlushResponseListener in your SpringBootServletInitializer subclass configure(). + * + * @param The incoming event type + * @param The expected return type + */ +public class SpringBootLambdaContainerHandler extends AwsLambdaServletContainerHandler { + private static final String DISPATCHER_SERVLET_REGISTRATION_NAME = "dispatcherServlet"; + + private final Class springBootInitializer; + private static final Logger log = LoggerFactory.getLogger(SpringBootLambdaContainerHandler.class); + private String[] springProfiles = null; + private WebApplicationType springWebApplicationType; + private ConfigurableApplicationContext applicationContext; + + private static SpringBootLambdaContainerHandler instance; + + // State vars + private boolean initialized; + + /** + * We need to rely on the static instance of this for SpringBoot because we need it to access the ServletContext. + * Normally, SpringBoot would initialize its own embedded container through the SpringApplication.run() + * method. However, in our case we need to rely on the pre-initialized handler and need to fetch information from it + * for our mock {@link ServerlessReactiveServletEmbeddedServerFactory}. + * + * @return The initialized instance + */ + public static SpringBootLambdaContainerHandler getInstance() { + return instance; + } + + /** + * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `AwsProxyResponse` objects and the given Spring profiles + * @param springBootInitializer {@code SpringBootServletInitializer} class + * @param profiles A list of Spring profiles to activate + * @return An initialized instance of the `SpringLambdaContainerHandler` + * @throws ContainerInitializationException If an error occurs while initializing the Spring framework + */ + public static SpringBootLambdaContainerHandler getAwsProxyHandler(Class springBootInitializer, String... profiles) + throws ContainerInitializationException { + return new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(springBootInitializer) + .profiles(profiles) + .buildAndInitialize(); + } + + /** + * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `HttpApiV2ProxyRequest` objects and the given Spring profiles + * @param springBootInitializer {@code SpringBootServletInitializer} class + * @param profiles A list of Spring profiles to activate + * @return An initialized instance of the `SpringLambdaContainerHandler` + * @throws ContainerInitializationException If an error occurs while initializing the Spring framework + */ + public static SpringBootLambdaContainerHandler getHttpApiV2ProxyHandler(Class springBootInitializer, String... profiles) + throws ContainerInitializationException { + return new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(springBootInitializer) + .profiles(profiles) + .buildAndInitialize(); + } + + /** + * Creates a new container handler with the given reader and writer objects + * + * @param requestTypeClass The class for the incoming Lambda event + * @param responseTypeClass The class for the Lambda function output + * @param requestReader An implementation of `RequestReader` + * @param responseWriter An implementation of `ResponseWriter` + * @param securityContextWriter An implementation of `SecurityContextWriter` + * @param exceptionHandler An implementation of `ExceptionHandler` + * @param springBootInitializer {@code SpringBootServletInitializer} class + * @param init The initialization Wrapper that will be used to start Spring Boot + * @param applicationType The Spring Web Application Type + */ + public SpringBootLambdaContainerHandler(Class requestTypeClass, + Class responseTypeClass, + RequestReader requestReader, + ResponseWriter responseWriter, + SecurityContextWriter securityContextWriter, + ExceptionHandler exceptionHandler, + Class springBootInitializer, + InitializationWrapper init, + WebApplicationType applicationType) { + super(requestTypeClass, responseTypeClass, requestReader, responseWriter, securityContextWriter, exceptionHandler); + Timer.start("SPRINGBOOT2_CONTAINER_HANDLER_CONSTRUCTOR"); + initialized = false; + this.springBootInitializer = springBootInitializer; + springWebApplicationType = applicationType; + setInitializationWrapper(init); + SpringBootLambdaContainerHandler.setInstance(this); + + Timer.stop("SPRINGBOOT2_CONTAINER_HANDLER_CONSTRUCTOR"); + } + + // this is not pretty. However, because SpringBoot wants to control all of the initialization + // we need to access this handler as a singleton from the EmbeddedContainer to set the servlet + // context and from the ServletConfigurationSupport implementation + private static void setInstance(SpringBootLambdaContainerHandler h) { + SpringBootLambdaContainerHandler.instance = h; + } + + public void activateSpringProfiles(String... profiles) { + springProfiles = profiles; + // force a re-initialization + initialized = false; + } + + @Override + protected AwsHttpServletResponse getContainerResponse(HttpServletRequest request, CountDownLatch latch) { + return new AwsHttpServletResponse(request, latch); + } + + @Override + protected void handleRequest(HttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + // this method of the AwsLambdaServletContainerHandler sets the servlet context + Timer.start("SPRINGBOOT2_HANDLE_REQUEST"); + + // wire up the application context on the first invocation + if (!initialized) { + initialize(); + } + + // process filters & invoke servlet + Servlet reqServlet = ((AwsServletContext)getServletContext()).getServletForPath(containerRequest.getPathInfo()); + if (AwsHttpServletRequest.class.isAssignableFrom(containerRequest.getClass())) { + ((AwsHttpServletRequest)containerRequest).setServletContext(getServletContext()); + ((AwsHttpServletRequest)containerRequest).setResponse(containerResponse); + } + doFilter(containerRequest, containerResponse, reqServlet); + Timer.stop("SPRINGBOOT2_HANDLE_REQUEST"); + } + + + @Override + public void initialize() + throws ContainerInitializationException { + Timer.start("SPRINGBOOT2_COLD_START"); + + SpringApplicationBuilder builder = new SpringApplicationBuilder(getEmbeddedContainerClasses()) + .web(springWebApplicationType); // .REACTIVE, .SERVLET + if (springProfiles != null) { + builder.profiles(springProfiles); + } + applicationContext = builder.run(); + if (springWebApplicationType == WebApplicationType.SERVLET) { + ((AnnotationConfigServletWebApplicationContext)applicationContext).setServletContext(getServletContext()); + AwsServletRegistration reg = (AwsServletRegistration)getServletContext().getServletRegistration(DISPATCHER_SERVLET_REGISTRATION_NAME); + if (reg != null) { + reg.setLoadOnStartup(1); + } + } + super.initialize(); + initialized = true; + Timer.stop("SPRINGBOOT2_COLD_START"); + } + + private Class[] getEmbeddedContainerClasses() { + Class[] classes = new Class[2]; + if (springWebApplicationType == WebApplicationType.REACTIVE) { + try { + // if HandlerAdapter is available we assume they are using WebFlux. Otherwise plain servlet. + this.getClass().getClassLoader().loadClass("org.springframework.web.reactive.HandlerAdapter"); + log.debug("Found WebFlux HandlerAdapter on classpath, using reactive server factory"); + classes[0] = ServerlessReactiveServletEmbeddedServerFactory.class; + } catch (ClassNotFoundException e) { + springWebApplicationType = WebApplicationType.SERVLET; + classes[0] = ServerlessServletEmbeddedServerFactory.class; + } + } else { + classes[0] = ServerlessServletEmbeddedServerFactory.class; + } + + classes[1] = springBootInitializer; + return classes; + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java new file mode 100644 index 000000000..e7ad017f1 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringBootProxyHandlerBuilder.java @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.ExceptionHandler; +import com.amazonaws.serverless.proxy.internal.servlet.ServletLambdaContainerHandlerBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import org.springframework.boot.WebApplicationType; + +import jakarta.servlet.http.HttpServletRequest; + +public final class SpringBootProxyHandlerBuilder extends ServletLambdaContainerHandlerBuilder< + RequestType, + AwsProxyResponse, + HttpServletRequest, + SpringBootLambdaContainerHandler, + SpringBootProxyHandlerBuilder> { + private Class springBootInitializer; + private String[] profiles; + private WebApplicationType applicationType = WebApplicationType.REACTIVE; + + @Override + protected SpringBootProxyHandlerBuilder self() { + return this; + } + + + public SpringBootProxyHandlerBuilder springBootApplication(Class app) { + springBootInitializer = app; + return self(); + } + + public SpringBootProxyHandlerBuilder profiles(String... profiles) { + this.profiles = profiles; + return self(); + } + + public SpringBootProxyHandlerBuilder servletApplication() { + this.applicationType = WebApplicationType.SERVLET; + return self(); + } + + @Override + public SpringBootLambdaContainerHandler build() throws ContainerInitializationException { + validate(); + if (springBootInitializer == null) { + throw new ContainerInitializationException("Missing spring boot application class in builder", null); + } + SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler( + requestTypeClass, + responseTypeClass, + requestReader, + responseWriter, + securityContextWriter, + exceptionHandler, + springBootInitializer, + initializationWrapper, + applicationType + ); + if (profiles != null) { + handler.activateSpringProfiles(profiles); + } + return handler; + } + + @Override + public SpringBootLambdaContainerHandler buildAndInitialize() throws ContainerInitializationException { + SpringBootLambdaContainerHandler handler = build(); + initializationWrapper.start(handler); + return handler; + } + + @Override + protected ExceptionHandler defaultExceptionHandler() { + return new SpringBootAwsProxyExceptionHandler(); + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandler.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandler.java new file mode 100644 index 000000000..af7970a15 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandler.java @@ -0,0 +1,91 @@ +package com.amazonaws.serverless.proxy.spring; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.AsyncInitializationWrapper; +import com.amazonaws.serverless.proxy.InitializationTypeHelper; +import com.amazonaws.serverless.proxy.internal.InitializableLambdaContainerHandler; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import org.springframework.cloud.function.serverless.web.FunctionClassUtils; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import tools.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; + +/* + * Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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. + */ + +/** + * An implementation of {@link RequestStreamHandler} which delegates to + * Spring Cloud Function serverless web module managed by Spring team. + * + * It requires no sub-classing from the user other then being identified as "Handler". + * The configuration class(es) should be provided via MAIN_CLASS environment variable. + * + */ +public class SpringDelegatingLambdaContainerHandler implements RequestStreamHandler { + private final ServerlessMVC mvc; + private final ObjectMapper mapper; + private final AwsProxyHttpServletResponseWriter responseWriter; + + public SpringDelegatingLambdaContainerHandler() throws ContainerInitializationException { + this(new Class[] {FunctionClassUtils.getStartClass()}); + } + + public SpringDelegatingLambdaContainerHandler(final Class... startupClasses) throws ContainerInitializationException { + SpringDelegatingInitHandler initHandler = new SpringDelegatingInitHandler(startupClasses); + if (InitializationTypeHelper.isAsyncInitializationDisabled()) { + initHandler.initialize(); + } else { + AsyncInitializationWrapper asyncInitWrapper = new AsyncInitializationWrapper(); + asyncInitWrapper.start(initHandler); + } + this.mvc = initHandler.getMvc(); + this.mapper = new ObjectMapper(); + this.responseWriter = new AwsProxyHttpServletResponseWriter(); + } + + @Override + public void handleRequest(InputStream input, OutputStream output, Context lambdaContext) throws IOException { + HttpServletRequest httpServletRequest = AwsSpringHttpProcessingUtils + .generateHttpServletRequest(input, lambdaContext, this.mvc.getServletContext(), this.mapper); + AwsProxyResponse awsProxyResponse = AwsSpringHttpProcessingUtils.processRequest(httpServletRequest, mvc, responseWriter); + this.mapper.writeValue(output, awsProxyResponse); + } + + private static final class SpringDelegatingInitHandler implements InitializableLambdaContainerHandler { + private ServerlessMVC mvc; + private final Class[] startupClasses; + + public SpringDelegatingInitHandler(final Class... startupClasses) { + this.startupClasses = startupClasses; + } + + @Override + public void initialize() throws ContainerInitializationException { + this.mvc = ServerlessMVC.INSTANCE(this.startupClasses); + this.mvc.waitForContext(); + } + + public ServerlessMVC getMvc() { + return this.mvc; + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java new file mode 100644 index 000000000..917523a3b --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessReactiveServletEmbeddedServerFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring.embedded; + +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.web.server.reactive.AbstractReactiveWebServerFactory; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.WebServerException; +import org.springframework.core.Ordered; +import org.springframework.http.server.reactive.HttpHandler; +import org.springframework.http.server.reactive.ServletHttpHandlerAdapter; + +import jakarta.servlet.*; +import java.io.IOException; +import java.util.Enumeration; + +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +public class ServerlessReactiveServletEmbeddedServerFactory extends AbstractReactiveWebServerFactory implements WebServer, Servlet { + private ServletHttpHandlerAdapter handler; + private ServletConfig config; + static final String SERVLET_NAME = "com.amazonaws.serverless.proxy.spring.embedded.ServerlessReactiveEmbeddedServerFactory"; + static final String SERVLET_INFO = "ServerlessReactiveEmbeddedServerFactory"; + + @Override + @SuppressFBWarnings("MTIA_SUSPECT_SERVLET_INSTANCE_FIELD") + public WebServer getWebServer(HttpHandler httpHandler) { + handler = new ServletHttpHandlerAdapter(httpHandler); + return this; + } + + @Override + public void start() throws WebServerException { + // register this object as the main handler servlet with a mapping of / + SpringBootLambdaContainerHandler + .getInstance() + .getServletContext() + .addServlet(SERVLET_NAME, this) + .addMapping("/"); + handler.init(new ServletAdapterConfig()); + } + + @Override + public void stop() throws WebServerException { + // nothing to do here. + } + + @Override + public void init(ServletConfig servletConfig) throws ServletException { + config = servletConfig; + } + + @Override + public ServletConfig getServletConfig() { + return config; + } + + @Override + public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { + handler.service(servletRequest, servletResponse); + } + + @Override + public String getServletInfo() { + return SERVLET_INFO; + } + + @Override + public void destroy() { + + } + + private static class ServletAdapterConfig implements ServletConfig { + @Override + public String getServletName() { + return SERVLET_NAME; + } + + @Override + public ServletContext getServletContext() { + return SpringBootLambdaContainerHandler.getInstance().getServletContext(); + } + + @Override + public String getInitParameter(String s) { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java new file mode 100644 index 000000000..7278ba444 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactory.java @@ -0,0 +1,65 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.proxy.spring.embedded; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsLambdaServletContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import org.springframework.boot.autoconfigure.AutoConfigureOrder; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.WebServerException; +import org.springframework.boot.web.servlet.ServletContextInitializer; +import org.springframework.boot.web.server.servlet.ServletWebServerFactory; +import org.springframework.core.Ordered; + +import jakarta.servlet.ServletException; + +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) +public class ServerlessServletEmbeddedServerFactory implements ServletWebServerFactory, WebServer { + @SuppressWarnings("rawtypes") + private AwsLambdaServletContainerHandler handler; + + public ServerlessServletEmbeddedServerFactory() { + super(); + handler = SpringBootLambdaContainerHandler.getInstance(); + } + + @Override + public WebServer getWebServer(ServletContextInitializer... initializers) { + for (ServletContextInitializer i : initializers) { + try { + if (handler.getServletContext() == null) { + throw new WebServerException("Attempting to initialize ServletEmbeddedWebServer without ServletContext in Handler", null); + } + i.onStartup(handler.getServletContext()); + } catch (ServletException e) { + throw new WebServerException("Could not initialize Servlets", e); + } + } + return this; + } + + @Override + public void start() throws WebServerException { + + } + + @Override + public void stop() throws WebServerException { + + } + + @Override + public int getPort() { + return 0; + } +} diff --git a/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring.factories b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..cd5c2e70b --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.context.ApplicationContextInitializer=\ +com.amazonaws.serverless.proxy.spring.AwsSpringWebRuntimeInitializer diff --git a/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring/aot.factories b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000..44acc0d83 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor=com.amazonaws.serverless.proxy.spring.AwsSpringAotTypesProcessor \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AWSWebRuntimeTests.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AWSWebRuntimeTests.java new file mode 100644 index 000000000..9903e8f8b --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AWSWebRuntimeTests.java @@ -0,0 +1,39 @@ +package com.amazonaws.serverless.proxy.spring; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.ConfigurableApplicationContext; + +public class AWSWebRuntimeTests { + + @AfterEach + public void after() { + System.clearProperty("_HANDLER"); + } + + @Test + public void testWebRuntimeInitialization() throws Exception { + try (ConfigurableApplicationContext context = SpringApplication.run(EmptyApplication.class);) { + assertFalse(context.getBeansOfType(AwsSpringWebCustomRuntimeEventLoop.class).size() > 0); + } + System.setProperty("_HANDLER", "foo"); + AwsSpringWebCustomRuntimeEventLoop loop = null; + try (ConfigurableApplicationContext context = SpringApplication.run(EmptyApplication.class);) { + Thread.sleep(100); + assertTrue(context.getBeansOfType(AwsSpringWebCustomRuntimeEventLoop.class).size() > 0); + loop = context.getBean(AwsSpringWebCustomRuntimeEventLoop.class); + assertTrue(loop.isRunning()); + } + assertFalse(loop.isRunning()); + } + + @EnableAutoConfiguration + private static class EmptyApplication { + + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java new file mode 100644 index 000000000..69836be71 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/AwsSpringHttpProcessingUtilsTests.java @@ -0,0 +1,290 @@ +package com.amazonaws.serverless.proxy.spring; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AlbContext; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.cloud.function.serverless.web.ServerlessMVC; +import org.springframework.cloud.function.serverless.web.ServerlessServletContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import tools.jackson.databind.ObjectMapper; + +import jakarta.servlet.http.HttpServletRequest; + +import static org.junit.jupiter.api.Assertions.*; + +public class AwsSpringHttpProcessingUtilsTests { + + private static String API_GATEWAY_EVENT = "{\n" + + " \"version\": \"1.0\",\n" + + " \"resource\": \"$default\",\n" + + " \"path\": \"/async\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"headers\": {\n" + + " \"Content-Length\": \"45\",\n" + + " \"Content-Type\": \"application/json\",\n" + + " \"Host\": \"i76bfh111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"User-Agent\": \"curl/7.79.1\",\n" + + " \"X-Amzn-Trace-Id\": \"Root=1-64087690-2151375b219d3ba3389ea84e\",\n" + + " \"X-Forwarded-For\": \"109.210.252.44\",\n" + + " \"X-Forwarded-Port\": \"443\",\n" + + " \"X-Forwarded-Proto\": \"https\",\n" + + " \"accept\": \"*/*\"\n" + + " },\n" + + " \"multiValueHeaders\": {\n" + + " \"Content-Length\": [\n" + + " \"45\"\n" + + " ],\n" + + " \"Content-Type\": [\n" + + " \"application/json\"\n" + + " ],\n" + + " \"Host\": [\n" + + " \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\"\n" + + " ],\n" + + " \"User-Agent\": [\n" + + " \"curl/7.79.1\"\n" + + " ],\n" + + " \"X-Amzn-Trace-Id\": [\n" + + " \"Root=1-64087690-2151375b219d3ba3389ea84e\"\n" + + " ],\n" + + " \"X-Forwarded-For\": [\n" + + " \"109.210.252.44\"\n" + + " ],\n" + + " \"X-Forwarded-Port\": [\n" + + " \"443\"\n" + + " ],\n" + + " \"X-Forwarded-Proto\": [\n" + + " \"https\"\n" + + " ],\n" + + " \"accept\": [\n" + + " \"*/*\"\n" + + " ]\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"abc\": \"xyz\",\n" + + " \"parameter1\": \"value2\"\n" + + " },\n" + + " \"multiValueQueryStringParameters\": {\n" + + " \"abc\": [\n" + + " \"xyz\"\n" + + " ],\n" + + " \"parameter1\": [\n" + + " \"value1\",\n" + + " \"value2\"\n" + + " ]\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789098\",\n" + + " \"apiId\": \"i76bfhczs0\",\n" + + " \"domainName\": \"i76bfhc111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"domainPrefix\": \"i76bfhczs0\",\n" + + " \"extendedRequestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"path\": \"/pets\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"requestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"requestTime\": \"08/Mar/2023:11:50:40 +0000\",\n" + + " \"requestTimeEpoch\": 1678276240455,\n" + + " \"resourceId\": \"$default\",\n" + + " \"resourcePath\": \"$default\",\n" + + " \"stage\": \"$default\"\n" + + " },\n" + + " \"pathParameters\": null,\n" + + " \"stageVariables\": null,\n" + + " \"body\": \"{\\\"name\\\":\\\"bob\\\"}\",\n" + + " \"isBase64Encoded\": false\n" + + "}"; + + private static String API_GATEWAY_EVENT_V2 = "{\n" + + " \"version\": \"2.0\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"rawPath\": \"/async\",\n" + + " \"rawQueryString\": \"parameter1=value1¶meter1=value2¶meter2=value\",\n" + + " \"cookies\": [\n" + + " \"cookie1\",\n" + + " \"cookie2\"\n" + + " ],\n" + + " \"headers\": {\n" + + " \"header1\": \"value1\",\n" + + " \"header2\": \"value1,value2\",\n" + + " \"User-Agent\": \"curl/7.79.1\",\n" + + " \"X-Forwarded-Port\": \"443\"\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"parameter1\": \"value1,value2\",\n" + + " \"parameter2\": \"value\"\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789012\",\n" + + " \"apiId\": \"api-id\",\n" + + " \"authentication\": {\n" + + " \"clientCert\": {\n" + + " \"clientCertPem\": \"CERT_CONTENT\",\n" + + " \"subjectDN\": \"www.example.com\",\n" + + " \"issuerDN\": \"Example issuer\",\n" + + " \"serialNumber\": \"a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1\",\n" + + " \"validity\": {\n" + + " \"notBefore\": \"May 28 12:30:02 2019 GMT\",\n" + + " \"notAfter\": \"Aug 5 09:36:04 2021 GMT\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"authorizer\": {\n" + + " \"jwt\": {\n" + + " \"claims\": {\n" + + " \"claim1\": \"value1\",\n" + + " \"claim2\": \"value2\"\n" + + " },\n" + + " \"scopes\": [\n" + + " \"scope1\",\n" + + " \"scope2\"\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"domainName\": \"id.execute-api.us-east-1.amazonaws.com\",\n" + + " \"domainPrefix\": \"id\",\n" + + " \"http\": {\n" + + " \"method\": \"POST\",\n" + + " \"path\": \"/async\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"sourceIp\": \"IP\",\n" + + " \"userAgent\": \"agent\"\n" + + " },\n" + + " \"requestId\": \"id\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"stage\": \"$default\",\n" + + " \"time\": \"12/Mar/2020:19:03:58 +0000\",\n" + + " \"timeEpoch\": 1583348638390\n" + + " },\n" + + " \"body\": \"Hello from Lambda\",\n" + + " \"pathParameters\": {\n" + + " \"parameter1\": \"value1\"\n" + + " },\n" + + " \"isBase64Encoded\": false,\n" + + " \"stageVariables\": {\n" + + " \"stageVariable1\": \"value1\",\n" + + " \"stageVariable2\": \"value2\"\n" + + " }\n" + + "}"; + + private static final String ALB_EVENT = "{\n" + + " \"requestContext\": {\n" + + " \"elb\": {\n" + + " \"targetGroupArn\": \"arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09\"\n" + + " }\n" + + " },\n" + + " \"httpMethod\": \"POST\",\n" + + " \"path\": \"/async\",\n" + + " \"multiValueQueryStringParameters\": { \"parameter1\": [\"value1\", \"value2\"], \"parameter2\": [\"1970-01-01T00%3A00%3A00.004Z\"]},\n" + + " \"multiValueHeaders\": {\n" + + " \"accept\": [\"text/html,application/xhtml+xml\"],\n" + + " \"accept-language\": [\"en-US,en;q=0.8\"],\n" + + " \"content-type\": [\"text/plain\"],\n" + + " \"cookie\": [\"cookies\"],\n" + + " \"host\": [\"lambda-846800462-us-east-2.elb.amazonaws.com\"],\n" + + " \"User-Agent\": [\"curl/7.79.1\"],\n" + + " \"x-amzn-trace-id\": [\"Root=1-5bdb40ca-556d8b0c50dc66f0511bf520\"],\n" + + " \"x-forwarded-for\": [\"72.21.198.66\"],\n" + + " \"x-forwarded-port\": [\"443\"],\n" + + " \"x-forwarded-proto\": [\"https\"]\n" + + " },\n" + + " \"isBase64Encoded\": false,\n" + + " \"body\": \"request_body\"\n" + + "}"; + + private final ObjectMapper mapper = new ObjectMapper(); + + public static Collection data() { + return Arrays.asList(new String[]{API_GATEWAY_EVENT, API_GATEWAY_EVENT_V2, ALB_EVENT}); + } + + @MethodSource("data") + @ParameterizedTest + public void validateHttpServletRequestGenerationWithInputStream(String jsonEvent) { + ByteArrayInputStream inputStream = new ByteArrayInputStream(jsonEvent.getBytes(StandardCharsets.UTF_8)); + ServerlessServletContext servletContext = new ServerlessServletContext(); + HttpServletRequest request = AwsSpringHttpProcessingUtils.generateHttpServletRequest(inputStream, null, servletContext, mapper); + assertRequest(request); + } + + private static void assertRequest(HttpServletRequest request) { + assertEquals("curl/7.79.1", request.getHeader("User-Agent")); + assertEquals("443", request.getHeader("X-Forwarded-Port")); + assertEquals("POST", request.getMethod()); + assertEquals("/async", request.getRequestURI()); + assertNotNull(request.getServletContext()); + // parameter handling for 2.0 requests is currently not spec compliant and to be fixed in future version + // see also GitHub issue https://github.com/aws/serverless-java-container/issues/1278 + if (!(request.getAttribute(RequestReader.HTTP_API_EVENT_PROPERTY) instanceof HttpApiV2ProxyRequest)) { + assertEquals("value1", request.getParameter("parameter1")); + assertArrayEquals(new String[]{"value1", "value2"}, request.getParameterValues("parameter1")); + } + if (request.getAttribute(RequestReader.ALB_CONTEXT_PROPERTY) instanceof AlbContext) { + // query params should be decoded + assertEquals("1970-01-01T00:00:00.004Z", request.getParameter("parameter2")); + } + } + + @MethodSource("data") + @ParameterizedTest + public void validateHttpServletRequestGenerationWithJson(String jsonEvent) { + ServerlessServletContext servletContext = new ServerlessServletContext(); + HttpServletRequest request = AwsSpringHttpProcessingUtils.generateHttpServletRequest(jsonEvent, null, servletContext, mapper); + // spot check some headers + assertRequest(request); + } + + @MethodSource("data") + @ParameterizedTest + public void validateRequestResponse(String jsonEvent) throws Exception { + try (ConfigurableApplicationContext context = SpringApplication.run(EmptyApplication.class);) { + ServerlessMVC mvc = ServerlessMVC.INSTANCE((ServletWebServerApplicationContext) context); + AwsProxyHttpServletResponseWriter responseWriter = new AwsProxyHttpServletResponseWriter(); + AwsProxyResponse awsResponse = AwsSpringHttpProcessingUtils.processRequest( + AwsSpringHttpProcessingUtils.generateHttpServletRequest(jsonEvent, null, + mvc.getServletContext(), mapper), mvc, responseWriter); + assertEquals("hello", awsResponse.getBody()); + assertEquals(200, awsResponse.getStatusCode()); + } + + } + + @EnableAutoConfiguration + @Configuration + public static class EmptyApplication { + @RestController + @EnableWebMvc + public static class MyController { + @PostMapping(path = "/async") + public String async(@RequestBody String body) { + return "hello"; + } + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf((csrf) -> csrf.disable()); + return http.build(); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java new file mode 100644 index 000000000..a111e510a --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/JpaAppTest.java @@ -0,0 +1,52 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.jpaapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.jpaapp.MessageController; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Arrays; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class JpaAppTest { + + LambdaHandler handler; + MockLambdaContext lambdaContext = new MockLambdaContext(); + + private String type; + + public static Collection data() { + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + } + + public void initJpaAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + + @MethodSource("data") + @ParameterizedTest + void asyncRequest(String reqType) { + initJpaAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/async", "POST") + .json() + .body("{\"name\":\"kong\"}"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals("{\"name\":\"KONG\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void helloRequest_respondsWithSingleMessage(String reqType) { + initJpaAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/hello", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals(MessageController.HELLO_MESSAGE, resp.getBody()); + } + +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java new file mode 100644 index 000000000..d0b579509 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SecurityAppTest.java @@ -0,0 +1,39 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.securityapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.securityapp.SecurityConfig; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SecurityAppTest { + + LambdaHandler handler = new LambdaHandler(); + MockLambdaContext lambdaContext = new MockLambdaContext(); + + public SecurityAppTest() { + System.setProperty("logging.level.root", "DEBUG"); + } + + @Test + void helloRequest_withAuth_respondsWithSingleMessage() { + AwsProxyRequest req = new AwsProxyRequestBuilder("/hello", "GET").build(); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals(401, resp.getStatusCode()); + assertTrue(resp.getMultiValueHeaders().containsKey(HttpHeaders.WWW_AUTHENTICATE)); + req = new AwsProxyRequestBuilder("/hello", "GET") + .basicAuth(SecurityConfig.USERNAME, SecurityConfig.PASSWORD) + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .build(); + resp = handler.handleRequest(req, lambdaContext); + assertEquals(200, resp.getStatusCode()); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java new file mode 100644 index 000000000..3e7cd0cf6 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/ServletAppTest.java @@ -0,0 +1,236 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.ContainerConfig; +import com.amazonaws.serverless.proxy.spring.servletapp.*; +import tools.jackson.core.JacksonException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +public class ServletAppTest { + + LambdaHandler handler; + MockLambdaContext lambdaContext = new MockLambdaContext(); + + private String type; + + public static Collection data() { + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + } + + public void initServletAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + + @MethodSource("data") + @ParameterizedTest + void asyncRequest(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/async", "POST") + .json() + .body("{\"name\":\"bob\"}"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals("{\"name\":\"BOB\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void helloRequest_respondsWithSingleMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/hello", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertEquals(MessageController.HELLO_MESSAGE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void validateRequest_invalidData_respondsWith400(String reqType) { + initServletAppTest(reqType); + UserData ud = new UserData(); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/validate", "POST") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .body(ud); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + try { + System.out.println(LambdaContainerHandler.getObjectMapper().writeValueAsString(resp)); + } catch (JacksonException e) { + e.printStackTrace(); + } + assertEquals("3", resp.getBody()); + assertEquals(400, resp.getStatusCode()); + + UserData ud2 = new UserData(); + ud2.setFirstName("Test"); + ud2.setLastName("Test"); + ud2.setEmail("Test"); + req = new AwsProxyRequestBuilder("/validate", "POST") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN) + .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON) + .body(ud2); + resp = handler.handleRequest(req, lambdaContext); + assertEquals("1", resp.getBody()); + assertEquals(400, resp.getStatusCode()); + } + + @MethodSource("data") + @ParameterizedTest + void messageObject_parsesObject_returnsCorrectMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .json() + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void messageObject_propertiesInContentType_returnsCorrectMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .header(HttpHeaders.CONTENT_TYPE, "application/json;v=1") + .header(HttpHeaders.ACCEPT, "application/json;v=1") + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void echoMessage_fileNameLikeParameter_returnsMessage(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/echo/test.test.test", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test.test.test", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void getUtf8String_returnsValidUtf8String(String reqType) { + initServletAppTest(reqType); + // We expect strings to come back as UTF-8 correctly because Spring itself will call the setCharacterEncoding + // method on the response to set it to UTF- + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/content-type/utf8", "GET") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("text/plain; charset=UTF-8", resp.getMultiValueHeaders().get(HttpHeaders.CONTENT_TYPE).stream().collect(Collectors.joining(","))); + assertEquals(MessageController.UTF8_RESPONSE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void getUtf8Json_returnsValidUtf8String(String reqType) { + initServletAppTest(reqType); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/content-type/jsonutf8", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("{\"s\":\"" + MessageController.UTF8_RESPONSE + "\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void stream_getUtf8String_returnsValidUtf8String(String reqType) throws IOException { + initServletAppTest(reqType); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + LambdaStreamHandler streamHandler = new LambdaStreamHandler(type); + AwsProxyRequestBuilder reqBuilder = new AwsProxyRequestBuilder("/content-type/utf8", "GET") + .header(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN); + InputStream req = null; + switch (type) { + case "ALB": + req = reqBuilder.alb().buildStream(); + break; + case "API_GW": + req = reqBuilder.buildStream(); + break; + case "HTTP_API": + req = reqBuilder.toHttpApiV2RequestStream(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamHandler.handleRequest(req, out, lambdaContext); + AwsProxyResponse resp = LambdaContainerHandler.getObjectMapper().readValue(out.toByteArray(), AwsProxyResponse.class); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals(MessageController.UTF8_RESPONSE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void stream_getUtf8Json_returnsValidUtf8String(String reqType) throws IOException { + initServletAppTest(reqType); + LambdaContainerHandler.getContainerConfig().setDefaultContentCharset(ContainerConfig.DEFAULT_CONTENT_CHARSET); + LambdaStreamHandler streamHandler = new LambdaStreamHandler(type); + AwsProxyRequestBuilder reqBuilder = new AwsProxyRequestBuilder("/content-type/jsonutf8", "GET"); + InputStream req = null; + switch (type) { + case "ALB": + req = reqBuilder.alb().buildStream(); + break; + case "API_GW": + req = reqBuilder.buildStream(); + break; + case "HTTP_API": + req = reqBuilder.toHttpApiV2RequestStream(); + } + ByteArrayOutputStream out = new ByteArrayOutputStream(); + streamHandler.handleRequest(req, out, lambdaContext); + AwsProxyResponse resp = LambdaContainerHandler.getObjectMapper().readValue(out.toByteArray(), AwsProxyResponse.class); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("{\"s\":\"" + MessageController.UTF8_RESPONSE + "\"}", resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void springExceptionMapping_throw404Ex_expectMappedTo404(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/ex/customstatus", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(404, resp.getStatusCode()); + } + + @MethodSource("data") + @ParameterizedTest + void echoMessage_populatesSingleValueHeadersForHttpApiV2(String reqType) { + initServletAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .header(HttpHeaders.CONTENT_TYPE, "application/json;v=1") + .header(HttpHeaders.ACCEPT, "application/json;v=1") + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + if ("HTTP_API".equals(type)) { + assertNotNull(resp.getHeaders()); + } else { + assertNull(resp.getHeaders()); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SlowAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SlowAppTest.java new file mode 100644 index 000000000..f5e83e85e --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SlowAppTest.java @@ -0,0 +1,32 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.slowapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.slowapp.MessageController; +import com.amazonaws.serverless.proxy.spring.slowapp.SlowTestApplication; +import org.junit.jupiter.api.Test; + +import java.time.Instant; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SlowAppTest { + + @Test + void slowAppInit_continuesInBackgroundThread_returnsCorrect() { + LambdaHandler slowApp = new LambdaHandler(); + System.out.println("Start time: " + slowApp.getConstructorTime()); + assertTrue(slowApp.getConstructorTime() < 10_000); + AwsProxyRequest req = new AwsProxyRequestBuilder("/hello", "GET").build(); + long startRequestTime = Instant.now().toEpochMilli(); + AwsProxyResponse resp = slowApp.handleRequest(req, new MockLambdaContext()); + long endRequestTime = Instant.now().toEpochMilli(); + assertTrue(endRequestTime - startRequestTime > SlowTestApplication.SlowDownInit.INIT_SLEEP_TIME_MS - 10_000); + assertEquals(200, resp.getStatusCode()); + assertEquals(MessageController.HELLO_MESSAGE, resp.getBody()); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java new file mode 100644 index 000000000..d34585302 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/SpringDelegatingLambdaContainerHandlerTests.java @@ -0,0 +1,350 @@ +package com.amazonaws.serverless.proxy.spring; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.util.CollectionUtils; + +import com.amazonaws.serverless.proxy.spring.servletapp.MessageData; +import com.amazonaws.serverless.proxy.spring.servletapp.ServletApplication; +import com.amazonaws.serverless.proxy.spring.servletapp.UserData; +import tools.jackson.databind.ObjectMapper; + +import jakarta.ws.rs.core.HttpHeaders; + +@SuppressWarnings("rawtypes") +public class SpringDelegatingLambdaContainerHandlerTests { + + private static String API_GATEWAY_EVENT = "{\n" + + " \"version\": \"1.0\",\n" + + " \"resource\": \"$default\",\n" + + " \"path\": \"/async\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"headers\": {\n" + + " \"Content-Length\": \"45\",\n" + + " \"Content-Type\": \"application/json\",\n" + + " \"Host\": \"i76bfh111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"User-Agent\": \"curl/7.79.1\",\n" + + " \"X-Amzn-Trace-Id\": \"Root=1-64087690-2151375b219d3ba3389ea84e\",\n" + + " \"X-Forwarded-For\": \"109.210.252.44\",\n" + + " \"X-Forwarded-Port\": \"443\",\n" + + " \"X-Forwarded-Proto\": \"https\",\n" + + " \"accept\": \"*/*\"\n" + + " },\n" + + " \"multiValueHeaders\": {\n" + + " \"Content-Length\": [\n" + + " \"45\"\n" + + " ],\n" + + " \"Content-Type\": [\n" + + " \"application/json\"\n" + + " ],\n" + + " \"Host\": [\n" + + " \"i76bfhczs0.execute-api.eu-west-3.amazonaws.com\"\n" + + " ],\n" + + " \"User-Agent\": [\n" + + " \"curl/7.79.1\"\n" + + " ],\n" + + " \"X-Amzn-Trace-Id\": [\n" + + " \"Root=1-64087690-2151375b219d3ba3389ea84e\"\n" + + " ],\n" + + " \"X-Forwarded-For\": [\n" + + " \"109.210.252.44\"\n" + + " ],\n" + + " \"X-Forwarded-Port\": [\n" + + " \"443\"\n" + + " ],\n" + + " \"X-Forwarded-Proto\": [\n" + + " \"https\"\n" + + " ],\n" + + " \"accept\": [\n" + + " \"*/*\"\n" + + " ]\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"abc\": \"xyz\",\n" + + " \"name\": \"Ricky\",\n" + + " \"foo\": \"baz\"\n" + + " },\n" + + " \"multiValueQueryStringParameters\": {\n" + + " \"abc\": [\n" + + " \"xyz\"\n" + + " ],\n" + + " \"name\": [\n" + + " \"Ricky\"\n" + + " ],\n" + + " \"foo\": [\n" + + " \"bar\",\n" + + " \"baz\"\n" + + " ]\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789098\",\n" + + " \"apiId\": \"i76bfhczs0\",\n" + + " \"domainName\": \"i76bfhc111.execute-api.eu-west-3.amazonaws.com\",\n" + + " \"domainPrefix\": \"i76bfhczs0\",\n" + + " \"extendedRequestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"httpMethod\": \"POST\",\n" + + " \"identity\": {\n" + + " \"accessKey\": null,\n" + + " \"accountId\": null,\n" + + " \"caller\": null,\n" + + " \"cognitoAmr\": null,\n" + + " \"cognitoAuthenticationProvider\": null,\n" + + " \"cognitoAuthenticationType\": null,\n" + + " \"cognitoIdentityId\": null,\n" + + " \"cognitoIdentityPoolId\": null,\n" + + " \"principalOrgId\": null,\n" + + " \"sourceIp\": \"109.210.252.44\",\n" + + " \"user\": null,\n" + + " \"userAgent\": \"curl/7.79.1\",\n" + + " \"userArn\": null\n" + + " },\n" + + " \"path\": \"/pets\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"requestId\": \"Bdd2ngt5iGYEMIg=\",\n" + + " \"requestTime\": \"08/Mar/2023:11:50:40 +0000\",\n" + + " \"requestTimeEpoch\": 1678276240455,\n" + + " \"resourceId\": \"$default\",\n" + + " \"resourcePath\": \"$default\",\n" + + " \"stage\": \"$default\"\n" + + " },\n" + + " \"pathParameters\": null,\n" + + " \"stageVariables\": null,\n" + + " \"body\": \"{\\\"name\\\":\\\"bob\\\"}\",\n" + + " \"isBase64Encoded\": false\n" + + "}"; + + private static String API_GATEWAY_EVENT_V2 = "{\n" + + " \"version\": \"2.0\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"rawPath\": \"/my/path\",\n" + + " \"rawQueryString\": \"parameter1=value1¶meter1=value2&name=Ricky¶meter2=value\",\n" + + " \"cookies\": [\n" + + " \"cookie1\",\n" + + " \"cookie2\"\n" + + " ],\n" + + " \"headers\": {\n" + + " \"header1\": \"value1\",\n" + + " \"header2\": \"value1,value2\"\n" + + " },\n" + + " \"queryStringParameters\": {\n" + + " \"parameter1\": \"value1,value2\",\n" + + " \"name\": \"Ricky\",\n" + + " \"parameter2\": \"value\"\n" + + " },\n" + + " \"requestContext\": {\n" + + " \"accountId\": \"123456789012\",\n" + + " \"apiId\": \"api-id\",\n" + + " \"authentication\": {\n" + + " \"clientCert\": {\n" + + " \"clientCertPem\": \"CERT_CONTENT\",\n" + + " \"subjectDN\": \"www.example.com\",\n" + + " \"issuerDN\": \"Example issuer\",\n" + + " \"serialNumber\": \"a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1\",\n" + + " \"validity\": {\n" + + " \"notBefore\": \"May 28 12:30:02 2019 GMT\",\n" + + " \"notAfter\": \"Aug 5 09:36:04 2021 GMT\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"authorizer\": {\n" + + " \"jwt\": {\n" + + " \"claims\": {\n" + + " \"claim1\": \"value1\",\n" + + " \"claim2\": \"value2\"\n" + + " },\n" + + " \"scopes\": [\n" + + " \"scope1\",\n" + + " \"scope2\"\n" + + " ]\n" + + " }\n" + + " },\n" + + " \"domainName\": \"id.execute-api.us-east-1.amazonaws.com\",\n" + + " \"domainPrefix\": \"id\",\n" + + " \"http\": {\n" + + " \"method\": \"POST\",\n" + + " \"path\": \"/my/path\",\n" + + " \"protocol\": \"HTTP/1.1\",\n" + + " \"sourceIp\": \"IP\",\n" + + " \"userAgent\": \"agent\"\n" + + " },\n" + + " \"requestId\": \"id\",\n" + + " \"routeKey\": \"$default\",\n" + + " \"stage\": \"$default\",\n" + + " \"time\": \"12/Mar/2020:19:03:58 +0000\",\n" + + " \"timeEpoch\": 1583348638390\n" + + " },\n" + + " \"body\": \"Hello from Lambda\",\n" + + " \"pathParameters\": {\n" + + " \"parameter1\": \"value1\"\n" + + " },\n" + + " \"isBase64Encoded\": false,\n" + + " \"stageVariables\": {\n" + + " \"stageVariable1\": \"value1\",\n" + + " \"stageVariable2\": \"value2\"\n" + + " }\n" + + "}"; + + private SpringDelegatingLambdaContainerHandler handler; + + private ObjectMapper mapper = new ObjectMapper(); + + public void initServletAppTest() throws ContainerInitializationException { + this.handler = new SpringDelegatingLambdaContainerHandler(ServletApplication.class); + } + + public static Collection data() { + return Arrays.asList(new String[]{API_GATEWAY_EVENT, API_GATEWAY_EVENT_V2}); + } + + @MethodSource("data") + @ParameterizedTest + public void validateComplesrequest(String jsonEvent) throws Exception { + initServletAppTest(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", + "/foo/male/list/24", "{\"name\":\"bob\"}", false,null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + String[] responseBody = ((String) result.get("body")).split("/"); + assertEquals("male", responseBody[0]); + assertEquals("24", responseBody[1]); + assertEquals("Ricky", responseBody[2]); + } + + @MethodSource("data") + @ParameterizedTest + public void testAsyncPost(String jsonEvent) throws Exception { + initServletAppTest(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/async", "{\"name\":\"bob\"}",false, null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + assertEquals("{\"name\":\"BOB\"}", result.get("body")); + } + + @MethodSource("data") + @ParameterizedTest + public void testValidate400(String jsonEvent) throws Exception { + initServletAppTest(); + UserData ud = new UserData(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/validate", mapper.writeValueAsString(ud),false, null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(400, result.get("statusCode")); + assertEquals("3", result.get("body")); + } + + @MethodSource("data") + @ParameterizedTest + public void testValidate200(String jsonEvent) throws Exception { + initServletAppTest(); + UserData ud = new UserData(); + ud.setFirstName("bob"); + ud.setLastName("smith"); + ud.setEmail("foo@bar.com"); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/validate", mapper.writeValueAsString(ud),false, null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + assertEquals("VALID", result.get("body")); + } + + @MethodSource("data") + @ParameterizedTest + public void testValidate200Base64(String jsonEvent) throws Exception { + initServletAppTest(); + UserData ud = new UserData(); + ud.setFirstName("bob"); + ud.setLastName("smith"); + ud.setEmail("foo@bar.com"); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/validate", + Base64.getMimeEncoder().encodeToString(mapper.writeValueAsString(ud).getBytes()),true, null)); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + assertEquals("VALID", result.get("body")); + } + + + @MethodSource("data") + @ParameterizedTest + public void messageObject_parsesObject_returnsCorrectMessage(String jsonEvent) throws Exception { + initServletAppTest(); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/message", + mapper.writeValueAsString(new MessageData("test message")),false, null)); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals(200, result.get("statusCode")); + assertEquals("test message", result.get("body")); + } + + + + @SuppressWarnings({"unchecked" }) + @MethodSource("data") + @ParameterizedTest + void messageObject_propertiesInContentType_returnsCorrectMessage(String jsonEvent) throws Exception { + initServletAppTest(); + + Map headers = new HashMap<>(); + headers.put(HttpHeaders.CONTENT_TYPE, "application/json;v=1"); + headers.put(HttpHeaders.ACCEPT, "application/json;v=1"); + InputStream targetStream = new ByteArrayInputStream(this.generateHttpRequest(jsonEvent, "POST", "/message", + mapper.writeValueAsString(new MessageData("test message")),false, headers)); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + handler.handleRequest(targetStream, output, null); + Map result = mapper.readValue(output.toString(StandardCharsets.UTF_8), Map.class); + assertEquals("test message", result.get("body")); + } + + private byte[] generateHttpRequest(String jsonEvent, String method, String path, String body,boolean isBase64Encoded, Map headers) throws Exception { + Map requestMap = mapper.readValue(jsonEvent, Map.class); + if (requestMap.get("version").equals("2.0")) { + return generateHttpRequest2(requestMap, method, path, body, isBase64Encoded,headers); + } + return generateHttpRequest(requestMap, method, path, body,isBase64Encoded, headers); + } + + @SuppressWarnings({ "unchecked"}) + private byte[] generateHttpRequest(Map requestMap, String method, String path, String body,boolean isBase64Encoded, Map headers) throws Exception { + requestMap.put("path", path); + requestMap.put("httpMethod", method); + requestMap.put("body", body); + requestMap.put("isBase64Encoded", isBase64Encoded); + if (!CollectionUtils.isEmpty(headers)) { + requestMap.put("headers", headers); + } + return mapper.writeValueAsBytes(requestMap); + } + + @SuppressWarnings({ "unchecked"}) + private byte[] generateHttpRequest2(Map requestMap, String method, String path, String body,boolean isBase64Encoded, Map headers) throws Exception { + Map map = mapper.readValue(API_GATEWAY_EVENT_V2, Map.class); + Map http = (Map) ((Map) map.get("requestContext")).get("http"); + http.put("path", path); + http.put("method", method); + map.put("body", body); + map.put("isBase64Encoded", isBase64Encoded); + if (!CollectionUtils.isEmpty(headers)) { + map.put("headers", headers); + } + return mapper.writeValueAsBytes(map); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java new file mode 100644 index 000000000..cc7d3365d --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java @@ -0,0 +1,68 @@ +package com.amazonaws.serverless.proxy.spring; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.webfluxapp.LambdaHandler; +import com.amazonaws.serverless.proxy.spring.webfluxapp.MessageController; +import com.amazonaws.serverless.proxy.spring.webfluxapp.MessageData; +import tools.jackson.core.JacksonException; + +public class WebFluxAppTest { + + LambdaHandler handler; + MockLambdaContext lambdaContext = new MockLambdaContext(); + + private String type; + + public static Collection data() { + return Arrays.asList(new Object[]{"API_GW", "ALB", "HTTP_API"}); + } + + public void initWebFluxAppTest(String reqType) { + type = reqType; + handler = new LambdaHandler(type); + } + + @MethodSource("data") + @ParameterizedTest + void helloRequest_respondsWithSingleMessage(String reqType) { + initWebFluxAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/single", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + System.out.println(resp.getBody()); + assertEquals(MessageController.MESSAGE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void helloDoubleRequest_respondsWithDoubleMessage(String reqType) { + initWebFluxAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/double", "GET"); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + + assertEquals(MessageController.MESSAGE + MessageController.MESSAGE, resp.getBody()); + } + + @MethodSource("data") + @ParameterizedTest + void messageObject_parsesObject_returnsCorrectMessage(String reqType) throws JacksonException { + initWebFluxAppTest(reqType); + AwsProxyRequestBuilder req = new AwsProxyRequestBuilder("/message", "POST") + .json() + .body(new MessageData("test message")); + AwsProxyResponse resp = handler.handleRequest(req, lambdaContext); + assertNotNull(resp); + assertEquals(200, resp.getStatusCode()); + assertEquals("test message", resp.getBody()); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java new file mode 100644 index 000000000..5ffd4a311 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/embedded/ServerlessServletEmbeddedServerFactoryTest.java @@ -0,0 +1,49 @@ +package com.amazonaws.serverless.proxy.spring.embedded; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.AwsProxyExceptionHandler; +import com.amazonaws.serverless.proxy.AwsProxySecurityContextWriter; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import org.junit.jupiter.api.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.web.servlet.ServletContextInitializer; + +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; + +import static org.junit.jupiter.api.Assertions.fail; + +public class ServerlessServletEmbeddedServerFactoryTest { + private SpringBootLambdaContainerHandler handler = new SpringBootLambdaContainerHandler<>( + AwsProxyRequest.class, + AwsProxyResponse.class, + new AwsProxyHttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsProxySecurityContextWriter(), + new AwsProxyExceptionHandler(), + null, + new InitializationWrapper(), + WebApplicationType.REACTIVE + ); + + public ServerlessServletEmbeddedServerFactoryTest() throws ContainerInitializationException { + } + + @Test + void getWebServer_callsInitializers() { + ServerlessServletEmbeddedServerFactory factory = new ServerlessServletEmbeddedServerFactory(); + factory.getWebServer(new ServletContextInitializer() { + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + if (servletContext == null) { + fail("Null servlet context"); + } + } + }); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/DatabaseConfig.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/DatabaseConfig.java new file mode 100644 index 000000000..aeef7c65e --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/DatabaseConfig.java @@ -0,0 +1,23 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; + +@Configuration +public class DatabaseConfig { + + @Bean + public DataSource dataSource() { + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUrl("jdbc:h2:mem:testdb"); + dataSource.setUsername("sa"); + dataSource.setPassword(""); + + return dataSource; + } +} + diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/JpaApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/JpaApplication.java new file mode 100644 index 000000000..5aced5e28 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/JpaApplication.java @@ -0,0 +1,17 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggingSystem; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +@SpringBootApplication(exclude = { + org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class +}) +@Import(MessageController.class) +public class JpaApplication {} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java new file mode 100644 index 000000000..0cf67c10f --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/LambdaHandler.java @@ -0,0 +1,59 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + private String type; + + public LambdaHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(JpaApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(JpaApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/MessageController.java new file mode 100644 index 000000000..a85292262 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/jpaapp/MessageController.java @@ -0,0 +1,31 @@ +package com.amazonaws.serverless.proxy.spring.jpaapp; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.context.request.async.DeferredResult; +import java.util.Collections; +import java.util.Map; + +@RestController +public class MessageController { + + public static final String HELLO_MESSAGE = "Hello"; + + @RequestMapping(path="/hello", method=RequestMethod.GET, produces = {"text/plain"}) + public String hello() { + return HELLO_MESSAGE; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @RequestMapping(path = "/async", method = RequestMethod.POST) + @ResponseBody + public DeferredResult> asyncResult(@RequestBody Map value) { + DeferredResult result = new DeferredResult<>(); + result.setResult(Collections.singletonMap("name", value.get("name").toUpperCase())); + return result; + } + +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java new file mode 100644 index 000000000..ae8ba21ac --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/LambdaHandler.java @@ -0,0 +1,25 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(SecurityApplication.class); + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + return handler.proxy(awsProxyRequest, context); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/MessageController.java new file mode 100644 index 000000000..ad67d4766 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/MessageController.java @@ -0,0 +1,17 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + + +@RestController +public class MessageController { + public static final String HELLO_MESSAGE = "Hello"; + + @RequestMapping(path="/hello", method=RequestMethod.GET, produces = {"text/plain"}) + public Mono hello() { + return Mono.just(HELLO_MESSAGE); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java new file mode 100644 index 000000000..d4036dcfe --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityApplication.java @@ -0,0 +1,17 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.web.reactive.config.EnableWebFlux; + +@SpringBootApplication(exclude = { + org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class, + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class +}) +@EnableWebFluxSecurity +@EnableWebFlux +@Import(SecurityConfig.class) +public class SecurityApplication { +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java new file mode 100644 index 000000000..d83b81db6 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/securityapp/SecurityConfig.java @@ -0,0 +1,44 @@ +package com.amazonaws.serverless.proxy.spring.securityapp; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.server.SecurityWebFilterChain; + +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig +{ + public static final String USERNAME = "admin"; + public static final String PASSWORD = "{noop}password"; + private static BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + @Bean + public SecurityWebFilterChain securitygWebFilterChain( + ServerHttpSecurity http) { + return http.authorizeExchange() + .anyExchange().authenticated().and().csrf().disable() + .httpBasic() + .and().build(); + } + + @Bean + public static BCryptPasswordEncoder passwordEncoder() { + return passwordEncoder; + } + + @Bean + public MapReactiveUserDetailsService userDetailsService() { + UserDetails user = User + .withUsername(USERNAME) + .password(passwordEncoder.encode(PASSWORD)) + .roles("USER") + .build(); + return new MapReactiveUserDetailsService(user); + } +} \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java new file mode 100644 index 000000000..88441988e --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaHandler.java @@ -0,0 +1,60 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + private String type; + + public LambdaHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java new file mode 100644 index 000000000..fd7d71d79 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/LambdaStreamHandler.java @@ -0,0 +1,63 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class LambdaStreamHandler implements RequestStreamHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + private String type; + + public LambdaStreamHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .servletApplication() + .springBootApplication(ServletApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + switch (type) { + case "API_GW": + case "ALB": + handler.proxyStream(inputStream, outputStream, context); + break; + case "HTTP_API": + httpApiHandler.proxyStream(inputStream, outputStream, context); + } + + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java new file mode 100644 index 000000000..1923396c6 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageController.java @@ -0,0 +1,75 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.Errors; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.server.ResponseStatusException; + +import jakarta.validation.Valid; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@RestController +public class MessageController { + public static final String HELLO_MESSAGE = "Hello"; + public static final String VALID_MESSAGE = "VALID"; + public static final String UTF8_RESPONSE = "öüäß фрыцшщ"; + public static final String EX_MESSAGE = "404 exception message"; + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @RequestMapping(path = "/async", method = RequestMethod.POST) + @ResponseBody + public DeferredResult> asyncResult(@RequestBody Map value) { + DeferredResult result = new DeferredResult<>(); + result.setResult(Collections.singletonMap("name", value.get("name").toUpperCase())); + return result; + } + + @RequestMapping(path="/hello", method=RequestMethod.GET, produces = {"text/plain"}) + public String hello() { + return HELLO_MESSAGE; + } + + @RequestMapping(path="/validate", method=RequestMethod.POST, produces = {"text/plain"}) + public ResponseEntity validateBody(@RequestBody @Valid UserData userData, Errors errors) { + if (errors != null && errors.hasErrors()) { + return ResponseEntity.badRequest().body(errors.getErrorCount() + ""); + } + return ResponseEntity.ok(VALID_MESSAGE); + } + + @RequestMapping(path="/message", method = RequestMethod.POST) + public String returnMessage(@RequestBody MessageData data) { + if (data == null) { + throw new RuntimeException("No message data"); + } + return data.getMessage(); + } + + @RequestMapping(path="/echo/{message}", method=RequestMethod.GET) + public String returnPathMessage(@PathVariable(value="message") String message) { + return message; + } + + @GetMapping(value = "/content-type/utf8", produces = "text/plain") + public ResponseEntity getUtf8String() { + return ResponseEntity.ok(UTF8_RESPONSE); + } + + @GetMapping(value = "/content-type/jsonutf8", produces=MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity> getUtf8Json() { + Map resp = new HashMap(); + resp.put("s", UTF8_RESPONSE); + return ResponseEntity.ok(resp); + } + + @GetMapping(value = "/ex/customstatus") + public String throw404Exception() { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, EX_MESSAGE); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java new file mode 100644 index 000000000..129101cbe --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/MessageData.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +public class MessageData { + private String message; + + public MessageData() { + } + + public MessageData(String m) { + setMessage(m); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java new file mode 100644 index 000000000..9f01859aa --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/ServletApplication.java @@ -0,0 +1,32 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@SpringBootApplication(exclude = { + org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class, + org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class, + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class +}) +@Import(MessageController.class) +@RestController +public class ServletApplication { + + @RequestMapping(path = "/foo/{gender}/list/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public String complexRequest( + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name + ) { + return gender + "/" + age + "/" + name; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java new file mode 100644 index 000000000..379291a39 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/servletapp/UserData.java @@ -0,0 +1,50 @@ +package com.amazonaws.serverless.proxy.spring.servletapp; + + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +public class UserData { + @NotBlank + private String firstName; + @NotBlank + private String lastName; + @NotNull @Email + private String email; + private String error; + + public UserData() { + + } + + public UserData(String err) { + error = err; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getError() { return error; } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java new file mode 100644 index 000000000..22f75e7a9 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/LambdaHandler.java @@ -0,0 +1,40 @@ +package com.amazonaws.serverless.proxy.spring.slowapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import java.time.Instant; + +public class LambdaHandler implements RequestHandler { + private SpringBootLambdaContainerHandler handler; + private long constructorTime; + + public LambdaHandler() { + try { + long startTime = Instant.now().toEpochMilli(); + System.out.println("startCall: " + startTime); + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .springBootApplication(SlowTestApplication.class) + .buildAndInitialize(); + constructorTime = Instant.now().toEpochMilli() - startTime; + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + public long getConstructorTime() { + return constructorTime; + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + return handler.proxy(awsProxyRequest, context); + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/MessageController.java new file mode 100644 index 000000000..098e8e7df --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/MessageController.java @@ -0,0 +1,15 @@ +package com.amazonaws.serverless.proxy.spring.slowapp; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class MessageController { + public static final String HELLO_MESSAGE = "Hello"; + + @RequestMapping(path="/hello", method= RequestMethod.GET) + public String hello() { + return HELLO_MESSAGE; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/SlowTestApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/SlowTestApplication.java new file mode 100644 index 000000000..006e51e45 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/slowapp/SlowTestApplication.java @@ -0,0 +1,26 @@ +package com.amazonaws.serverless.proxy.spring.slowapp; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.stereotype.Component; + +import java.time.Instant; + +@SpringBootApplication(exclude = { + org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class, + org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class, + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class +}) +public class SlowTestApplication { + + @Component + public static class SlowDownInit implements InitializingBean { + public static final int INIT_SLEEP_TIME_MS = 13_000; + + @Override + public void afterPropertiesSet() throws Exception { + Thread.sleep(INIT_SLEEP_TIME_MS); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java new file mode 100644 index 000000000..0eb52a7bc --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/LambdaHandler.java @@ -0,0 +1,58 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.InitializationWrapper; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.model.HttpApiV2ProxyRequest; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +public class LambdaHandler implements RequestHandler { + private static SpringBootLambdaContainerHandler handler; + private static SpringBootLambdaContainerHandler httpApiHandler; + + private String type; + + public LambdaHandler(String reqType) { + type = reqType; + try { + switch (type) { + case "API_GW": + case "ALB": + handler = new SpringBootProxyHandlerBuilder() + .defaultProxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(WebFluxTestApplication.class) + .buildAndInitialize(); + break; + case "HTTP_API": + httpApiHandler = new SpringBootProxyHandlerBuilder() + .defaultHttpApiV2Proxy() + .initializationWrapper(new InitializationWrapper()) + .springBootApplication(WebFluxTestApplication.class) + .buildAndInitialize(); + break; + } + } catch (ContainerInitializationException e) { + e.printStackTrace(); + } + } + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequestBuilder awsProxyRequest, Context context) { + switch (type) { + case "API_GW": + return handler.proxy(awsProxyRequest.build(), context); + case "ALB": + return handler.proxy(awsProxyRequest.alb().build(), context); + case "HTTP_API": + return httpApiHandler.proxy(awsProxyRequest.toHttpApiV2Request(), context); + default: + throw new RuntimeException("Unknown request type: " + type); + } + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java new file mode 100644 index 000000000..a04604618 --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java @@ -0,0 +1,36 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import reactor.core.publisher.Flux; + +@RestController +public class MessageController { + public static final String MESSAGE = "Hello"; + + @RequestMapping(path="/single", method= RequestMethod.GET, produces = {"text/plain"}) + Flux singleMessage(){ + return Flux.just( + MESSAGE + ); + } + + @RequestMapping(path="/double", method= RequestMethod.GET, produces={"text/plain"}) + Flux doubleMessage(){ + return Flux.just( + MESSAGE, + MESSAGE + ); + } + + @RequestMapping(path="/message", method = RequestMethod.POST, produces={"text/plain"}, consumes = {"application/json"}) + public Flux returnMessage(@RequestBody MessageData data) { + if (data == null) { + throw new RuntimeException("No message data"); + } + return Flux.just(data.getMessage()); + } +} \ No newline at end of file diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java new file mode 100644 index 000000000..2be6b4f2d --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageData.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +public class MessageData { + private String message; + + public MessageData() { + } + + public MessageData(String m) { + setMessage(m); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java new file mode 100644 index 000000000..fc6aecd6f --- /dev/null +++ b/aws-serverless-java-container-springboot4/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/WebFluxTestApplication.java @@ -0,0 +1,22 @@ +package com.amazonaws.serverless.proxy.spring.webfluxapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Import; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@SpringBootApplication(exclude = { + org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration.class, + org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class, + org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration.class, + org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration.class +}) +public class WebFluxTestApplication { + +} diff --git a/aws-serverless-jersey-archetype/pom.xml b/aws-serverless-jersey-archetype/pom.xml index 4afdc9996..8c1d68edc 100644 --- a/aws-serverless-jersey-archetype/pom.xml +++ b/aws-serverless-jersey-archetype/pom.xml @@ -4,12 +4,12 @@ com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless.archetypes aws-serverless-jersey-archetype - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT maven-archetype diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml index 1f05e9018..da0c05405 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,7 +16,7 @@ 1.8 3.1.10 - 2.18.3 + 3.0.2 5.12.1 @@ -24,12 +24,12 @@ com.amazonaws.serverless aws-serverless-java-container-jersey - ${project.version} + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - ${project.version} + 3.0.0-SNAPSHOT tests test-jar test @@ -51,18 +51,18 @@ jackson-annotations - com.fasterxml.jackson.core + tools.jackson.core jackson-databind - com.fasterxml.jackson.core + tools.jackson.core jackson-core - com.fasterxml.jackson.core + tools.jackson.core jackson-databind \${jackson.version} diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java index 768a5ad98..a4e2251e4 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java @@ -10,6 +10,7 @@ import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.ServerProperties; +import tools.jackson.core.JacksonException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; diff --git a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java index 53db9b161..a247daed7 100644 --- a/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java +++ b/aws-serverless-jersey-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java @@ -16,6 +16,7 @@ import jakarta.ws.rs.core.Response; import java.io.ByteArrayOutputStream; +import tools.jackson.core.JacksonException; import java.io.IOException; import java.io.InputStream; @@ -81,7 +82,7 @@ private void handle(InputStream is, ByteArrayOutputStream os) { private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { try { return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); - } catch (IOException e) { + } catch (JacksonException e) { e.printStackTrace(); fail("Error while parsing response: " + e.getMessage()); } diff --git a/aws-serverless-spring-archetype/pom.xml b/aws-serverless-spring-archetype/pom.xml index 2a2f59b8b..fc00bb2ce 100644 --- a/aws-serverless-spring-archetype/pom.xml +++ b/aws-serverless-spring-archetype/pom.xml @@ -4,12 +4,12 @@ com.amazonaws.serverless aws-serverless-java-container - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT com.amazonaws.serverless.archetypes aws-serverless-spring-archetype - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT maven-archetype diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml index 48b70f27f..4dd847a38 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/pom.xml @@ -16,8 +16,8 @@ 1.8 1.8 - 6.2.6 - 5.12.1 + 7.0.0 + 6.0.0 2.24.2 @@ -25,12 +25,12 @@ com.amazonaws.serverless aws-serverless-java-container-spring - ${project.version} + 3.0.0-SNAPSHOT com.amazonaws.serverless aws-serverless-java-container-core - ${project.version} + 3.0.0-SNAPSHOT tests test-jar test diff --git a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java index 53db9b161..cfb809f88 100644 --- a/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java +++ b/aws-serverless-spring-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java @@ -81,7 +81,7 @@ private void handle(InputStream is, ByteArrayOutputStream os) { private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { try { return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); - } catch (IOException e) { + } catch (tools.jackson.core.JacksonException e) { e.printStackTrace(); fail("Error while parsing response: " + e.getMessage()); } diff --git a/aws-serverless-springboot4-archetype/pom.xml b/aws-serverless-springboot4-archetype/pom.xml new file mode 100644 index 000000000..82e1ad033 --- /dev/null +++ b/aws-serverless-springboot4-archetype/pom.xml @@ -0,0 +1,80 @@ + + 4.0.0 + + + com.amazonaws.serverless + aws-serverless-java-container + 3.0.0-SNAPSHOT + + + com.amazonaws.serverless.archetypes + aws-serverless-springboot4-archetype + 3.0.0-SNAPSHOT + maven-archetype + + + https://github.com/aws/serverless-java-container.git + HEAD + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + + src/main/resources + true + + archetype-resources/pom.xml + archetype-resources/README.md + + + + src/main/resources + false + + archetype-resources/pom.xml + + + + + + + org.apache.maven.archetype + archetype-packaging + 3.4.0 + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.3.1 + + \ + + + + org.apache.maven.plugins + maven-archetype-plugin + 3.4.0 + + + + integration-test + + + + + + + + diff --git a/aws-serverless-springboot4-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/aws-serverless-springboot4-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml new file mode 100644 index 000000000..5379692ba --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -0,0 +1,39 @@ + + + + src/main/java + + **/*.java + + + + src/main/resources + + **/*.properties + + + + src/test/java + + **/*.java + + + + src/assembly + + * + + + + + + template.yml + README.md + build.gradle + + + + \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/README.md b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/README.md new file mode 100644 index 000000000..311c40aee --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/README.md @@ -0,0 +1,99 @@ +#set($resourceName = $artifactId) +#macro(replaceChar $originalName, $char) + #if($originalName.contains($char)) + #set($tokens = $originalName.split($char)) + #set($newResourceName = "") + #foreach($token in $tokens) + #set($newResourceName = $newResourceName + $token.substring(0,1).toUpperCase() + $token.substring(1).toLowerCase()) + #end + ${newResourceName} + #else + #set($newResourceName = $originalName.substring(0,1).toUpperCase() + $originalName.substring(1)) + ${newResourceName} + #end +#end +#set($resourceName = "#replaceChar($resourceName, '-')") +#set($resourceName = "#replaceChar($resourceName, '.')") +#set($resourceName = $resourceName.replaceAll("\n", "").trim()) +# \${artifactId} serverless API +The \${artifactId} project, created with [`aws-serverless-java-container`](https://github.com/aws/serverless-java-container). + +The starter project defines a simple `/ping` resource that can accept `GET` requests with its tests. + +The project folder also includes a `template.yml` file. You can use this [SAM](https://github.com/awslabs/serverless-application-model) file to deploy the project to AWS Lambda and Amazon API Gateway or test in local with the [SAM CLI](https://github.com/awslabs/aws-sam-cli). + +#[[##]]# Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +#[[##]]# Building the project +You can use the SAM CLI to quickly build the project +```bash +$ mvn archetype:generate -DartifactId=\${artifactId} -DarchetypeGroupId=com.amazonaws.serverless.archetypes -DarchetypeArtifactId=aws-serverless-jersey-archetype -DarchetypeVersion=${project.version} -DgroupId=\${groupId} -Dversion=\${version} -Dinteractive=false +$ cd \${artifactId} +$ sam build +Building resource '\${resourceName}Function' +Running JavaGradleWorkflow:GradleBuild +Running JavaGradleWorkflow:CopyArtifacts + +Build Succeeded + +Built Artifacts : .aws-sam/build +Built Template : .aws-sam/build/template.yaml + +Commands you can use next +========================= +[*] Invoke Function: sam local invoke +[*] Deploy: sam deploy --guided +``` + +#[[##]]# Testing locally with the SAM CLI + +From the project root folder - where the `template.yml` file is located - start the API with the SAM CLI. + +```bash +$ sam local start-api + +... +Mounting ${groupId}.StreamLambdaHandler::handleRequest (java11) at http://127.0.0.1:3000/{proxy+} [OPTIONS GET HEAD POST PUT DELETE PATCH] +... +``` + +Using a new shell, you can send a test ping request to your API: + +```bash +$ curl -s http://127.0.0.1:3000/ping | python -m json.tool + +{ + "pong": "Hello, World!" +} +``` + +#[[##]]# Deploying to AWS +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +------------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +------------------------------------------------------------------------------------------------------------- +\${resourceName}Api - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets +------------------------------------------------------------------------------------------------------------- +``` + +Copy the `OutputValue` into a browser or use curl to test your first request: + +```bash +$ curl -s https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/ping | python -m json.tool + +{ + "pong": "Hello, World!" +} +``` diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/build.gradle b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/build.gradle new file mode 100644 index 000000000..3aa54825c --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/build.gradle @@ -0,0 +1,37 @@ +apply plugin: 'java' + +repositories { + mavenLocal() + mavenCentral() + maven {url "https://repo.spring.io/milestone"} + maven {url "https://repo.spring.io/snapshot"} +} + +dependencies { + implementation ( + 'org.springframework.boot:spring-boot-starter-web:3.4.5', + 'com.amazonaws.serverless:aws-serverless-java-container-springboot3:[2.0-SNAPSHOT,)', + ) + + testImplementation("com.amazonaws.serverless:aws-serverless-java-container-core:[2.0-SNAPSHOT,):tests") + testImplementation("org.apache.httpcomponents.client5:httpclient5:5.5") + testImplementation(platform("org.junit:junit-bom:5.13.1")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +test { + useJUnitPlatform() +} + +build.dependsOn buildZip diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 000000000..73d407dd8 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/pom.xml @@ -0,0 +1,180 @@ +#set($dollar = '$') + + + 4.0.0 + + \${groupId} + \${artifactId} + \${version} + jar + + Serverless Spring Boot 3 API + https://github.com/aws/serverless-java-container + + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + + 17 + 5.12.1 + + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot3 + ${project.version} + + + com.amazonaws.serverless + aws-serverless-java-container-core + ${project.version} + tests + test-jar + test + + + org.apache.httpcomponents.client5 + httpclient5 + 5.4.3 + test + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.junit.jupiter + junit-jupiter + test + + + + + + + org.junit + junit-bom + ${junit.version} + import + pom + + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.2 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${dollar}{project.build.directory}${dollar}{file.separator}lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${dollar}{project.artifactId}-${dollar}{project.version} + + src${dollar}{file.separator}assembly${dollar}{file.separator}bin.xml + + false + + + + + + + + + diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/assembly/bin.xml b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/Application.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/Application.java new file mode 100644 index 000000000..1b74086f7 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/Application.java @@ -0,0 +1,24 @@ +#macro(loggingOff) + logging.level.root:OFF +#end +#set($logging = "#loggingOff()") +#set($logging = $logging.replaceAll("\n", "").trim()) +package ${groupId}; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +import ${groupId}.controller.PingController; + + +@SpringBootApplication +// We use direct @Import instead of @ComponentScan to speed up cold starts +// @ComponentScan(basePackages = "${groupId}.controller") +@Import({ PingController.class }) +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java new file mode 100644 index 000000000..e022540c1 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/StreamLambdaHandler.java @@ -0,0 +1,33 @@ +package ${groupId}; + + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +public class StreamLambdaHandler implements RequestStreamHandler { + private static SpringBootLambdaContainerHandler handler; + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/controller/PingController.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/controller/PingController.java new file mode 100644 index 000000000..94f517f07 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/java/controller/PingController.java @@ -0,0 +1,20 @@ +package ${groupId}.controller; + + +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.HashMap; +import java.util.Map; + + +@RestController +@EnableWebMvc +public class PingController { + @RequestMapping(path = "/ping", method = RequestMethod.GET) + public Map ping() { + Map pong = new HashMap<>(); + pong.put("pong", "Hello, World!"); + return pong; + } +} diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/resources/application.properties b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/resources/application.properties new file mode 100644 index 000000000..070e632fe --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/main/resources/application.properties @@ -0,0 +1,3 @@ +# Reduce logging level to make sure the application works with SAM local +# https://github.com/aws/serverless-java-container/issues/134 +logging.level.root=WARN \ No newline at end of file diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java new file mode 100644 index 000000000..26d5360bf --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/src/test/java/StreamLambdaHandlerTest.java @@ -0,0 +1,89 @@ +package ${groupId}; + + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.services.lambda.runtime.Context; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.ws.rs.HttpMethod; +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class StreamLambdaHandlerTest { + + private static StreamLambdaHandler handler; + private static Context lambdaContext; + + @BeforeAll + public static void setUp() { + handler = new StreamLambdaHandler(); + lambdaContext = new MockLambdaContext(); + } + + @Test + public void ping_streamRequest_respondsWithHello() { + InputStream requestStream = new AwsProxyRequestBuilder("/ping", HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .buildStream(); + ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); + + handle(requestStream, responseStream); + + AwsProxyResponse response = readResponse(responseStream); + assertNotNull(response); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatusCode()); + + assertFalse(response.isBase64Encoded()); + + assertTrue(response.getBody().contains("pong")); + assertTrue(response.getBody().contains("Hello, World!")); + + assertTrue(response.getMultiValueHeaders().containsKey(HttpHeaders.CONTENT_TYPE)); + assertTrue(response.getMultiValueHeaders().getFirst(HttpHeaders.CONTENT_TYPE).startsWith(MediaType.APPLICATION_JSON)); + } + + @Test + public void invalidResource_streamRequest_responds404() { + InputStream requestStream = new AwsProxyRequestBuilder("/pong", HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON) + .buildStream(); + ByteArrayOutputStream responseStream = new ByteArrayOutputStream(); + + handle(requestStream, responseStream); + + AwsProxyResponse response = readResponse(responseStream); + assertNotNull(response); + assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatusCode()); + } + + private void handle(InputStream is, ByteArrayOutputStream os) { + try { + handler.handleRequest(is, os, lambdaContext); + } catch (IOException e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + private AwsProxyResponse readResponse(ByteArrayOutputStream responseStream) { + try { + return LambdaContainerHandler.getObjectMapper().readValue(responseStream.toByteArray(), AwsProxyResponse.class); + } catch (IOException e) { + e.printStackTrace(); + fail("Error while parsing response: " + e.getMessage()); + } + return null; + } +} diff --git a/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/template.yml b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/template.yml new file mode 100644 index 000000000..18c231878 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/main/resources/archetype-resources/template.yml @@ -0,0 +1,52 @@ +#set($resourceName = $artifactId) +#macro(replaceChar $originalName, $char) + #if($originalName.contains($char)) + #set($tokens = $originalName.split($char)) + #set($newResourceName = "") + #foreach($token in $tokens) + #set($newResourceName = $newResourceName + $token.substring(0,1).toUpperCase() + $token.substring(1).toLowerCase()) + #end + ${newResourceName} + #else + #set($newResourceName = $originalName.substring(0,1).toUpperCase() + $originalName.substring(1)) + ${newResourceName} + #end +#end +#set($resourceName = "#replaceChar($resourceName, '-')") +#set($resourceName = "#replaceChar($resourceName, '.')") +#set($resourceName = $resourceName.replaceAll("\n", "").trim()) +#macro(regionVar) + AWS::Region +#end +#set($awsRegion = "#regionVar()") +#set($awsRegion = $awsRegion.replaceAll("\n", "").trim()) +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: AWS Serverless Spring Boot 2 API - ${groupId}::${artifactId} +Globals: + Api: + EndpointConfiguration: REGIONAL + +Resources: + ${resourceName}Function: + Type: AWS::Serverless::Function + Properties: + Handler: ${groupId}.StreamLambdaHandler::handleRequest + Runtime: java25 + CodeUri: . + MemorySize: 512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 30 + Events: + ProxyResource: + Type: Api + Properties: + Path: /{proxy+} + Method: any + +Outputs: + ${resourceName}Api: + Description: URL for application + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${${awsRegion}}.amazonaws.com/Prod/ping' + Export: + Name: ${resourceName}Api diff --git a/aws-serverless-springboot4-archetype/src/test/resources/projects/base/archetype.properties b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/archetype.properties new file mode 100644 index 000000000..7df3bf6e1 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/archetype.properties @@ -0,0 +1,3 @@ +groupId=test.service +artifactId=springboot-archetype-test +version=1.0-SNAPSHOT diff --git a/aws-serverless-springboot4-archetype/src/test/resources/projects/base/goal.txt b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/goal.txt new file mode 100644 index 000000000..597acc768 --- /dev/null +++ b/aws-serverless-springboot4-archetype/src/test/resources/projects/base/goal.txt @@ -0,0 +1 @@ +package \ No newline at end of file diff --git a/pom.xml b/pom.xml index ebc634fbb..94456d1a8 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.amazonaws.serverless aws-serverless-java-container pom - 2.1.5-SNAPSHOT + 3.0.0-SNAPSHOT AWS Serverless Java container A Java framework to run Spring, Spring Boot, Jersey, Spark, and Struts applications inside AWS Lambda https://github.com/aws/serverless-java-container @@ -29,9 +29,11 @@ aws-serverless-java-container-jersey aws-serverless-java-container-spring aws-serverless-java-container-springboot3 + aws-serverless-java-container-springboot4 aws-serverless-jersey-archetype aws-serverless-spring-archetype aws-serverless-springboot3-archetype + aws-serverless-springboot4-archetype @@ -78,9 +80,9 @@ 0.7 12.1.1 - 2.19.1 + 2.0.17 - 5.12.2 + 6.0.0 5.19.0 1.3 UTF-8 diff --git a/samples/springboot3/pet-store/build.gradle b/samples/springboot3/pet-store/build.gradle index 653135db0..893605416 100644 --- a/samples/springboot3/pet-store/build.gradle +++ b/samples/springboot3/pet-store/build.gradle @@ -1,5 +1,10 @@ apply plugin: 'java' +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + repositories { mavenLocal() mavenCentral() diff --git a/samples/springboot4/alt-pet-store/README.md b/samples/springboot4/alt-pet-store/README.md new file mode 100644 index 000000000..56cfca327 --- /dev/null +++ b/samples/springboot4/alt-pet-store/README.md @@ -0,0 +1,56 @@ +# Serverless Spring Boot 4 example +A basic pet store written with the [Spring Boot 4 framework](https://projects.spring.io/spring-boot/) and Spring Framework 7.0. Unlike older examples, this example is relying on the new +`SpringDelegatingLambdaContainerHandler`, which you simply need to identify as a _handler_ of the Lambda function. The main configuration class identified as `MAIN_CLASS` +environment variable or `Start-Class` or `Main-Class` entry in Manifest file. See provided `template.yml` file for reference. + + +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- + +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` + +You can also try a complex request passing both path and request parameters to complex endpoint such as this: + + +``` +@RequestMapping(path = "/foo/{gender}/bar/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) +public String complexRequest(@RequestBody String body, + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name +) +``` +For example. + +``` +curl -d '{"key1":"value1", "key2":"value2"}' -H "Content-Type: application/json" -X POST https://zuhd709386.execute-api.us-east-2.amazonaws.com/foo/male/bar/25?name=Ricky +``` diff --git a/samples/springboot4/alt-pet-store/build.gradle b/samples/springboot4/alt-pet-store/build.gradle new file mode 100644 index 000000000..7a6b13a49 --- /dev/null +++ b/samples/springboot4/alt-pet-store/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'java' + +java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 +} + +repositories { + mavenLocal() + mavenCentral() + maven {url "https://repo.spring.io/milestone"} + maven {url "https://repo.spring.io/snapshot"} +} + +dependencies { + implementation ( + implementation('org.springframework.boot:spring-boot-starter-web:3.4.5') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + }, + 'com.amazonaws.serverless:aws-serverless-java-container-springboot4:[2.0-SNAPSHOT,)', + 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + ) +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +build.dependsOn buildZip diff --git a/samples/springboot4/alt-pet-store/pom.xml b/samples/springboot4/alt-pet-store/pom.xml new file mode 100644 index 000000000..6f497e68b --- /dev/null +++ b/samples/springboot4/alt-pet-store/pom.xml @@ -0,0 +1,148 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + petstore-springboot4-example + 2.0-SNAPSHOT + Spring Boot 4 example for the aws-serverless-java-container library + Simple pet store written with Spring Framework 7.0 and Spring Boot 4.0 + https://aws.amazon.com/lambda/ + + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 25 + + + + + org.springframework.boot + spring-boot-starter + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.2.0-SNAPSHOT,),[2.1.1,) + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + + + diff --git a/samples/springboot4/alt-pet-store/src/assembly/bin.xml b/samples/springboot4/alt-pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java new file mode 100644 index 000000000..f5d30c519 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java @@ -0,0 +1,51 @@ +package com.amazonaws.serverless.sample.springboot4; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import com.amazonaws.serverless.sample.springboot4.controller.PetsController; +import com.amazonaws.serverless.sample.springboot4.filter.CognitoIdentityFilter; + +import jakarta.servlet.Filter; + + +@SpringBootApplication +@Import({ PetsController.class }) +public class Application { + + // silence console logging + @Value("${logging.level.root:OFF}") + String message = ""; + + /* + * Create required HandlerMapping, to avoid several default HandlerMapping instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + @Bean("CognitoIdentityFilter") + public Filter cognitoFilter() { + return new CognitoIdentityFilter(); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..f31542e55 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,90 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.controller; + + + +import com.amazonaws.serverless.sample.springboot4.model.Pet; +import com.amazonaws.serverless.sample.springboot4.model.PetData; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + + @RequestMapping(path = "/pets", method = RequestMethod.POST) + public Pet createPet(@RequestBody Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + + @RequestMapping(path = "/foo/{gender}/bar/{age}", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) + public String complexRequest(@RequestBody String body, + @PathVariable("gender") String gender, + @PathVariable("age") String age, + @RequestParam("name") String name + ) { + System.out.println("Body: " + body + " - " + gender + "/" + age + "/" + name); + return gender + "/" + age + "/" + name; + } + +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java new file mode 100644 index 000000000..ddc63025b --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b7e95ca97 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java new file mode 100644 index 000000000..66bdd3663 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot4/alt-pet-store/src/main/resources/logback.xml b/samples/springboot4/alt-pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..81d891777 --- /dev/null +++ b/samples/springboot4/alt-pet-store/src/main/resources/logback.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/springboot4/alt-pet-store/template.yml b/samples/springboot4/alt-pet-store/template.yml new file mode 100644 index 000000000..4a7e8bb43 --- /dev/null +++ b/samples/springboot4/alt-pet-store/template.yml @@ -0,0 +1,41 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot4 spring-cloud-function web-proxy support + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: +# AutoPublishAlias: bcn + FunctionName: pet-store-boot-4 + Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler::handleRequest + Runtime: java25 + SnapStart: + ApplyOn: PublishedVersions + CodeUri: . + MemorySize: 1024 + Policies: AWSLambdaBasicExecutionRole + Timeout: 30 + Environment: + Variables: + MAIN_CLASS: com.amazonaws.serverless.sample.springboot4.Application + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: SpringPetStoreApi + + diff --git a/samples/springboot4/graphql-pet-store/README.md b/samples/springboot4/graphql-pet-store/README.md new file mode 100644 index 000000000..9f1f8db3a --- /dev/null +++ b/samples/springboot4/graphql-pet-store/README.md @@ -0,0 +1,38 @@ +# Serverless Spring Boot 4 with GraphQL example +A basic pet store written with the [Spring Boot 4 framework](https://projects.spring.io/spring-boot/) and Spring Framework 7.0. Unlike older examples, this example uses the [Spring for GraphQl](https://docs.spring.io/spring-graphql/reference/) library. + + +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/graphQl +--------------------------------------------------------------------------------------------------------- + +$ curl -X POST https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/graphQl -d '{"query":"query petDetails {\n petById(id: \"pet-1\") {\n id\n name\n breed\n owner {\n id\n firstName\n lastName\n }\n }\n}","operationName":"petDetails"}' -H "Content-Type: application/json" + +``` \ No newline at end of file diff --git a/samples/springboot4/graphql-pet-store/pom.xml b/samples/springboot4/graphql-pet-store/pom.xml new file mode 100644 index 000000000..08835769c --- /dev/null +++ b/samples/springboot4/graphql-pet-store/pom.xml @@ -0,0 +1,168 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-springboot4-graphql-example + 2.0-SNAPSHOT + Spring Boot 4 GraphQL example for the aws-serverless-java-container library + GraphQL pet store written with Spring Framework 7.0 and Spring Boot 4.0 + https://aws.amazon.com/lambda/ + + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 25 + + + + + org.springframework.boot + spring-boot-starter-graphql + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + org.springframework.graphql + spring-graphql-test + test + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.0.0-SNAPSHOT,),[2.0.0-M1,) + + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + + + diff --git a/samples/springboot4/graphql-pet-store/src/assembly/bin.xml b/samples/springboot4/graphql-pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..efc312c25 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java new file mode 100644 index 000000000..b60367223 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java @@ -0,0 +1,43 @@ +package com.amazonaws.serverless.sample.springboot4; + +import com.amazonaws.serverless.sample.springboot4.controller.PetsController; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + + +@SpringBootApplication +@Import({ PetsController.class }) +public class Application { + + // silence console logging + @Value("${logging.level.root:OFF}") + String message = ""; + + /* + * Create required HandlerMapping, to avoid several default HandlerMapping instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java new file mode 100644 index 000000000..863af6351 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java @@ -0,0 +1,44 @@ +package com.amazonaws.serverless.sample.springboot4; + + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.Timer; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler; +import com.amazonaws.serverless.sample.springboot4.filter.CognitoIdentityFilter; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.EnumSet; + + +public class StreamLambdaHandler implements RequestStreamHandler { + private static SpringDelegatingLambdaContainerHandler handler; + static { + try { + handler = new SpringDelegatingLambdaContainerHandler(Application.class); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + public StreamLambdaHandler() { + // we enable the timer for debugging. This SHOULD NOT be enabled in production. + Timer.enable(); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.handleRequest(inputStream, outputStream, context); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..c76a624e1 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,21 @@ +package com.amazonaws.serverless.sample.springboot4.controller; + +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.stereotype.Controller; +import com.amazonaws.serverless.sample.springboot4.model.Owner; +import com.amazonaws.serverless.sample.springboot4.model.Pet; + +@Controller +public class PetsController { + @QueryMapping + public Pet petById(@Argument String id) { + return Pet.getById(id); + } + + @SchemaMapping + public Owner owner(Pet pet) { + return Owner.getById(pet.ownerId()); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Owner.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Owner.java new file mode 100644 index 000000000..5349a85b3 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Owner.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Arrays; +import java.util.List; + +public record Owner (String id, String firstName, String lastName) { + + private static List owners = Arrays.asList( + new Owner("owner-1", "Joshua", "Bloch"), + new Owner("owner-2", "Douglas", "Adams"), + new Owner("owner-3", "Bill", "Bryson") + ); + + public static Owner getById(String id) { + return owners.stream() + .filter(owner -> owner.id().equals(id)) + .findFirst() + .orElse(null); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b14199453 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,20 @@ +package com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Arrays; +import java.util.List; + +public record Pet (String id, String name, String breed, String ownerId) { + + private static List pets = Arrays.asList( + new Pet("pet-1", "Alpha", "Bulldog", "owner-1"), + new Pet("pet-2", "Max", "German Shepherd", "owner-2"), + new Pet("pet-3", "Rockie", "Golden Retriever", "owner-3") + ); + + public static Pet getById(String id) { + return pets.stream() + .filter(pet -> pet.id().equals(id)) + .findFirst() + .orElse(null); + } +} diff --git a/samples/springboot4/graphql-pet-store/src/main/resources/graphql/schema.graphqls b/samples/springboot4/graphql-pet-store/src/main/resources/graphql/schema.graphqls new file mode 100644 index 000000000..293cdcc40 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,16 @@ +type Query { + petById(id: ID): Pet +} + +type Pet { + id: ID + name: String + breed: String + owner: Owner +} + +type Owner { + id: ID + firstName: String + lastName: String +} diff --git a/samples/springboot4/graphql-pet-store/src/main/resources/logback.xml b/samples/springboot4/graphql-pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..8ff988992 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/src/main/resources/logback.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/samples/springboot4/graphql-pet-store/template.yml b/samples/springboot4/graphql-pet-store/template.yml new file mode 100644 index 000000000..5db3eefd3 --- /dev/null +++ b/samples/springboot4/graphql-pet-store/template.yml @@ -0,0 +1,35 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot4, Spring for GraphQl and the aws-serverless-java-container library + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler::handleRequest + Runtime: java25 + CodeUri: . + MemorySize: 1024 + Policies: AWSLambdaBasicExecutionRole + Timeout: 60 + Environment: + Variables: + MAIN_CLASS: com.amazonaws.serverless.sample.springboot4.Application + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringBootPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/graphql' + Export: + Name: SpringBootPetStoreApi diff --git a/samples/springboot4/pet-store-native/.gitignore b/samples/springboot4/pet-store-native/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/samples/springboot4/pet-store-native/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.jar b/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..cb28b0e37 Binary files /dev/null and b/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.jar differ diff --git a/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.properties b/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..7d02699af --- /dev/null +++ b/samples/springboot4/pet-store-native/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/samples/springboot4/pet-store-native/Dockerfile b/samples/springboot4/pet-store-native/Dockerfile new file mode 100644 index 000000000..ec8eb4a0d --- /dev/null +++ b/samples/springboot4/pet-store-native/Dockerfile @@ -0,0 +1,37 @@ +FROM public.ecr.aws/amazonlinux/amazonlinux:2023 + +RUN yum -y update \ + && yum install -y unzip tar gzip bzip2-devel ed gcc gcc-c++ gcc-gfortran \ + less libcurl-devel openssl openssl-devel readline-devel xz-devel \ + zlib-devel glibc-static zlib-static \ + && rm -rf /var/cache/yum + +# Graal VM +ENV GRAAL_VERSION 25.0.1 +ENV ARCHITECTURE aarch64 +ENV GRAAL_FILENAME graalvm-community-jdk-${GRAAL_VERSION}_linux-${ARCHITECTURE}_bin.tar.gz +RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/jdk-${GRAAL_VERSION}/${GRAAL_FILENAME} | tar -xvz +RUN mv graalvm-community-openjdk-${GRAAL_VERSION}* /usr/lib/graalvm +ENV JAVA_HOME /usr/lib/graalvm + +# Maven +ENV MVN_VERSION 3.9.9 +ENV MVN_FOLDERNAME apache-maven-${MVN_VERSION} +ENV MVN_FILENAME apache-maven-${MVN_VERSION}-bin.tar.gz +RUN curl -4 -L https://archive.apache.org/dist/maven/maven-3/${MVN_VERSION}/binaries/${MVN_FILENAME} | tar -xvz +RUN mv $MVN_FOLDERNAME /usr/lib/maven +RUN ln -s /usr/lib/maven/bin/mvn /usr/bin/mvn + +# Gradle +ENV GRADLE_VERSION 7.4.1 +ENV GRADLE_FOLDERNAME gradle-${GRADLE_VERSION} +ENV GRADLE_FILENAME gradle-${GRADLE_VERSION}-bin.zip +RUN curl -LO https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip +RUN unzip gradle-${GRADLE_VERSION}-bin.zip +RUN mv $GRADLE_FOLDERNAME /usr/lib/gradle +RUN ln -s /usr/lib/gradle/bin/gradle /usr/bin/gradle + +VOLUME /project +WORKDIR /project + +WORKDIR /pet-store-native diff --git a/samples/springboot4/pet-store-native/README.md b/samples/springboot4/pet-store-native/README.md new file mode 100644 index 000000000..57994a4e6 --- /dev/null +++ b/samples/springboot4/pet-store-native/README.md @@ -0,0 +1,39 @@ +In this sample, you'll build a native GraalVM image for running Spring Boot 4.0 and Spring Framework 7.0 web workloads in AWS Lambda. + +**Important**: Spring Boot 4.0 requires GraalVM 25 for native image compilation. GraalVM 21 is not compatible. + +## To build the sample + +You first need to build the function, then you will deploy it to AWS Lambda. + +Please note that the sample is for `x86` architectures. In case you want to build and run it on ARM, e.g. Apple Mac M1, M2, ... +you must change the according line in the `Dockerfile` to `ENV ARCHITECTURE aarch64`. +In addition, uncomment the `arm64` Architectures section in `template.yml`. + +### Step 1 - Build the native image + +Before starting the build, you must clone or download the code in **pet-store-native**. + +1. Change into the project directory: `samples/springboot4/pet-store-native` +2. Run the following to build a Docker container image with GraalVM 25 which will include all the necessary dependencies to build the application + ``` + docker build -t al2023-graalvm25:native-web . + ``` +3. Build the application within the previously created build image + ``` + docker run -it -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 al2023-graalvm25:native-web mvn clean native:compile -Pnative + ``` +4. After the build finishes, you need to deploy the function: + ``` + sam deploy --guided + ``` + +This will deploy your application and will attach an AWS API Gateway +Once the deployment is finished you should see the following: +``` +Key ServerlessWebNativeApi +Description URL for application +Value https://xxxxxxxx.execute-api.us-east-2.amazonaws.com/pets +``` + +You can now simply execute GET on this URL and see the listing fo all pets. diff --git a/samples/springboot4/pet-store-native/mvnw b/samples/springboot4/pet-store-native/mvnw new file mode 100755 index 000000000..8d937f4c1 --- /dev/null +++ b/samples/springboot4/pet-store-native/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/samples/springboot4/pet-store-native/mvnw.cmd b/samples/springboot4/pet-store-native/mvnw.cmd new file mode 100644 index 000000000..f80fbad3e --- /dev/null +++ b/samples/springboot4/pet-store-native/mvnw.cmd @@ -0,0 +1,205 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/samples/springboot4/pet-store-native/pom.xml b/samples/springboot4/pet-store-native/pom.xml new file mode 100644 index 000000000..fef6e7dfc --- /dev/null +++ b/samples/springboot4/pet-store-native/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + com.amazonaws.serverless.sample + pet-store-native-springboot4 + 0.0.1-SNAPSHOT + pet-store-native-springboot4 + Sample of AWS with Spring Boot 4.0 Native + + 25 + + + + org.springframework.boot + spring-boot-starter + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.0.0-SNAPSHOT,),[2.0.0-M1,) + + + + org.crac + crac + runtime + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + com.amazonaws + aws-lambda-java-events + 3.15.0 + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + native + + + + org.springframework.boot + spring-boot-maven-plugin + + -agentlib:native-image-agent=config-merge-dir=src/main/resources/META-INF/native-image/ --enable-preview + + + + + org.graalvm.buildtools + native-maven-plugin + + pet-store-native + + --enable-url-protocols=http + -march=compatibility + + + + + + build + + package + + + test + + test + + test + + + + + maven-assembly-plugin + + + native-zip + package + + single + + false + + + + + src/assembly/native.xml + + + + + + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/assembly/java.xml b/samples/springboot4/pet-store-native/src/assembly/java.xml new file mode 100644 index 000000000..bd4961b58 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/assembly/java.xml @@ -0,0 +1,31 @@ + + java-zip + + zip + + + + + target/classes + / + + + src/shell/java + / + true + 0775 + + bootstrap + + + + + + /lib + false + runtime + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/assembly/native.xml b/samples/springboot4/pet-store-native/src/assembly/native.xml new file mode 100644 index 000000000..9bd97a5b7 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/assembly/native.xml @@ -0,0 +1,29 @@ + + native-zip + + zip + + + + + src/shell/native + / + true + 0775 + + bootstrap + + + + target + / + true + 0775 + + pet-store-native + + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/DemoApplication.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/DemoApplication.java new file mode 100644 index 000000000..3f1d42559 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/DemoApplication.java @@ -0,0 +1,12 @@ +package com.amazonaws.serverless.sample.springboot4; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) throws Exception { + SpringApplication.run(DemoApplication.class, args); + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/HelloController.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/HelloController.java new file mode 100644 index 000000000..4f0abad79 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/HelloController.java @@ -0,0 +1,17 @@ +package com.amazonaws.serverless.sample.springboot4; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + public HelloController() { + System.out.println("Creating controller"); + } + + @GetMapping("/hello") + public String something(){ + return "Hello World"; + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..7576b7898 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.controller; + + + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import com.amazonaws.serverless.sample.springboot4.model.Pet; +import com.amazonaws.serverless.sample.springboot4.model.PetData; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + @PostMapping(path = "/pets") + public Pet createPet(@RequestBody Pet newPet) { + System.out.println("==> Creating Pet: " + newPet); + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @GetMapping(path = "/pets") + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + System.out.println("==> Listing Pets"); + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } + + @GetMapping(path = "/pets/{petId}") + public Pet listPets() { + System.out.println("==> Listing Pets"); + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java new file mode 100644 index 000000000..ddc63025b --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b7e95ca97 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java new file mode 100644 index 000000000..66bdd3663 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot4/pet-store-native/src/main/resources/META-INF/.gitignore b/samples/springboot4/pet-store-native/src/main/resources/META-INF/.gitignore new file mode 100644 index 000000000..0726bbaa2 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/resources/META-INF/.gitignore @@ -0,0 +1 @@ +/native-image/ diff --git a/samples/springboot4/pet-store-native/src/main/resources/application.properties b/samples/springboot4/pet-store-native/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/samples/springboot4/pet-store-native/src/shell/java/bootstrap b/samples/springboot4/pet-store-native/src/shell/java/bootstrap new file mode 100644 index 000000000..e30ee22e9 --- /dev/null +++ b/samples/springboot4/pet-store-native/src/shell/java/bootstrap @@ -0,0 +1,7 @@ +#!/bin/sh + +cd ${LAMBDA_TASK_ROOT:-.} + +java -Dspring.main.web-application-type=none -Dlogging.level.org.springframework=DEBUG \ + -noverify -XX:TieredStopAtLevel=1 -Xss256K -XX:MaxMetaspaceSize=128M \ + -cp .:`echo lib/*.jar | tr ' ' :` com.amazonaws.serverless.sample.springboot4.DemoApplication \ No newline at end of file diff --git a/samples/springboot4/pet-store-native/src/shell/native/bootstrap b/samples/springboot4/pet-store-native/src/shell/native/bootstrap new file mode 100644 index 000000000..0156b090b --- /dev/null +++ b/samples/springboot4/pet-store-native/src/shell/native/bootstrap @@ -0,0 +1,5 @@ +#!/bin/sh + +cd ${LAMBDA_TASK_ROOT:-.} + +./pet-store-native -Dlogging.level.org.springframework=DEBUG -Dlogging.level.com.amazonaws.serverless.proxy.spring=DEBUG diff --git a/samples/springboot4/pet-store-native/template.yaml b/samples/springboot4/pet-store-native/template.yaml new file mode 100644 index 000000000..d0b63d9a6 --- /dev/null +++ b/samples/springboot4/pet-store-native/template.yaml @@ -0,0 +1,33 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Serverless Java Container GraalVM with Spring Boot 4 +Resources: + ServerlessWebNativeFunction: + Type: AWS::Serverless::Function + Properties: + MemorySize: 512 + FunctionName: pet-store-native-springboot4 + Timeout: 15 + CodeUri: ./target/pet-store-native-springboot4-0.0.1-SNAPSHOT-native-zip.zip + Handler: NOP + Runtime: provided.al2023 + Architectures: + - arm64 + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL +Outputs: + ServerlessWebNativeApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: ServerlessWebNativeApi + \ No newline at end of file diff --git a/samples/springboot4/pet-store/README.md b/samples/springboot4/pet-store/README.md new file mode 100644 index 000000000..40955e968 --- /dev/null +++ b/samples/springboot4/pet-store/README.md @@ -0,0 +1,36 @@ +# Serverless Spring Boot 4 example +A basic pet store written with the [Spring Boot 4 framework](https://projects.spring.io/spring-boot/) and Spring Framework 7.0. The `StreamLambdaHandler` object is the main entry point for Lambda. + +The application can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) +* [Gradle](https://gradle.org/) or [Maven](https://maven.apache.org/) + +## Deployment +In a shell, navigate to the sample's folder and use the SAM CLI to build a deployable package +``` +$ sam build +``` + +This command compiles the application and prepares a deployment package in the `.aws-sam` sub-directory. + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment is completed, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- + +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/springboot4/pet-store/build.gradle b/samples/springboot4/pet-store/build.gradle new file mode 100644 index 000000000..7a6b13a49 --- /dev/null +++ b/samples/springboot4/pet-store/build.gradle @@ -0,0 +1,35 @@ +apply plugin: 'java' + +java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 +} + +repositories { + mavenLocal() + mavenCentral() + maven {url "https://repo.spring.io/milestone"} + maven {url "https://repo.spring.io/snapshot"} +} + +dependencies { + implementation ( + implementation('org.springframework.boot:spring-boot-starter-web:3.4.5') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + }, + 'com.amazonaws.serverless:aws-serverless-java-container-springboot4:[2.0-SNAPSHOT,)', + 'com.fasterxml.jackson.core:jackson-databind:2.18.2' + ) +} + +task buildZip(type: Zip) { + from compileJava + from processResources + into('lib') { + from(configurations.compileClasspath) { + exclude 'tomcat-embed-*' + } + } +} + +build.dependsOn buildZip diff --git a/samples/springboot4/pet-store/pom.xml b/samples/springboot4/pet-store/pom.xml new file mode 100644 index 000000000..937998eaf --- /dev/null +++ b/samples/springboot4/pet-store/pom.xml @@ -0,0 +1,160 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-springboot4-example + 2.0-SNAPSHOT + Spring Boot 4 example for the aws-serverless-java-container library + Simple pet store written with Spring Framework 7.0 and Spring Boot 4.0 + https://aws.amazon.com/lambda/ + + + org.springframework.boot + spring-boot-starter-parent + 4.0.0 + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 25 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot4 + [2.0.0-SNAPSHOT,),[2.0.0-M1,) + + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + + + + shaded-jar + + + + org.apache.maven.plugins + maven-shade-plugin + 3.6.0 + + false + + + + package + + shade + + + + + org.apache.tomcat.embed:* + + + + + + + + + + + assembly-zip + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + none + + + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/lib + runtime + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.7.1 + + + zip-assembly + package + + single + + + ${project.artifactId}-${project.version} + + src${file.separator}assembly${file.separator}bin.xml + + false + + + + + + + + + + + diff --git a/samples/springboot4/pet-store/src/assembly/bin.xml b/samples/springboot4/pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/samples/springboot4/pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java new file mode 100644 index 000000000..13a80b32a --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/Application.java @@ -0,0 +1,49 @@ +package com.amazonaws.serverless.sample.springboot4; + +import com.amazonaws.serverless.sample.springboot4.controller.PetsController; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.HandlerAdapter; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + + +@SpringBootApplication +@Import({ PetsController.class }) +public class Application { + + // silence console logging + @Value("${logging.level.root:OFF}") + String message = ""; + + /* + * Create required HandlerMapping, to avoid several default HandlerMapping instances being created + */ + @Bean + public HandlerMapping handlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /* + * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created + */ + @Bean + public HandlerAdapter handlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} \ No newline at end of file diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java new file mode 100644 index 000000000..61cde4fac --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/StreamLambdaHandler.java @@ -0,0 +1,50 @@ +package com.amazonaws.serverless.sample.springboot4; + + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.Timer; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.sample.springboot4.filter.CognitoIdentityFilter; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; + +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.EnumSet; + + +public class StreamLambdaHandler implements RequestStreamHandler { + private static SpringBootLambdaContainerHandler handler; + static { + try { + handler = SpringBootLambdaContainerHandler.getAwsProxyHandler(Application.class); + + // we use the onStartup method of the handler to register our custom filter + handler.onStartup(servletContext -> { + FilterRegistration.Dynamic registration = servletContext.addFilter("CognitoIdentityFilter", CognitoIdentityFilter.class); + registration.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); + }); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + e.printStackTrace(); + throw new RuntimeException("Could not initialize Spring Boot application", e); + } + } + + public StreamLambdaHandler() { + // we enable the timer for debugging. This SHOULD NOT be enabled in production. + Timer.enable(); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java new file mode 100644 index 000000000..cb80068bc --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/controller/PetsController.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.controller; + + + +import com.amazonaws.serverless.sample.springboot4.model.Pet; +import com.amazonaws.serverless.sample.springboot4.model.PetData; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.security.Principal; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + @RequestMapping(path = "/pets", method = RequestMethod.POST) + public Pet createPet(@RequestBody Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public Pet[] listPets(@RequestParam("limit") Optional limit, Principal principal) { + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + Pet[] outputPets = new Pet[queryLimit]; + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets[i] = newPet; + } + + return outputPets; + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..705683ae2 --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot4.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot4.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java new file mode 100644 index 000000000..ddc63025b --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java new file mode 100644 index 000000000..b7e95ca97 --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/Pet.java @@ -0,0 +1,55 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java new file mode 100644 index 000000000..66bdd3663 --- /dev/null +++ b/samples/springboot4/pet-store/src/main/java/com/amazonaws/serverless/sample/springboot4/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file 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 com.amazonaws.serverless.sample.springboot4.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot4/pet-store/src/main/resources/logback.xml b/samples/springboot4/pet-store/src/main/resources/logback.xml new file mode 100644 index 000000000..14a3a84fa --- /dev/null +++ b/samples/springboot4/pet-store/src/main/resources/logback.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/springboot4/pet-store/template.yml b/samples/springboot4/pet-store/template.yml new file mode 100644 index 000000000..789057a50 --- /dev/null +++ b/samples/springboot4/pet-store/template.yml @@ -0,0 +1,35 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot4 with the aws-serverless-java-container library + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler::handleRequest + Runtime: java25 + CodeUri: . + MemorySize: 1512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 60 + Environment: + Variables: + MAIN_CLASS: com.amazonaws.serverless.sample.springboot4.Application + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringBootPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: SpringBootPetStoreApi