diff --git a/README.md b/README.md index 4abbb9e2d..d37621e82 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Serverless Java container -The `aws-serverless-java-container` is collection of interfaces and their implementations that let you run Java application written with frameworks such as [Jersey](https://jersey.java.net/) or [Spark]() in [AWS Lambda](https://aws.amazon.com/lambda/). +The `aws-serverless-java-container` is collection of interfaces and their implementations that let you run Java application written with frameworks such as [Jersey](https://jersey.java.net/) or [Spark](http://sparkjava.com/) in [AWS Lambda](https://aws.amazon.com/lambda/). The library contains a core artifact called `aws-serverless-java-container-core` that defines the interfaces and base classes required as well as default implementation of the Java servlet `HttpServletRequest` and `HttpServletResponse`. The library also includes two initial implementations of the interfaces to support Jersey apps (`aws-serverless-java-container-jersey`) and Spark (`aws-serverless-java-container-spark`). @@ -18,7 +18,10 @@ To include the library in your Maven project, add the desired implementation to The simplest way to run your application serverlessly is to configure [API Gateway](https://aws.amazon.com/api-gateway/) to use the [`AWS_PROXY`](http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-set-up-simple-proxy.html#api-gateway-set-up-lambda-proxy-integration-on-proxy-resource) integration type and configure your desired `LambdaContainerHandler` implementation to use `AwsProxyRequest`/`AwsProxyResponse` readers and writers. Both Spark and Jersey implementations provide static helper methods that -pre-configure this for you, so your handler method would look like this: +pre-configure this for you. + +### Jersey support +The library expects to receive a valid Jax RS application object. For the Jersey implementation this is the `ResourceConfig` object. ```java public class LambdaHandler implements RequestHandler { @@ -26,8 +29,28 @@ public class LambdaHandler implements RequestHandler handler = JerseyLambdaContainerHandler.getAwsProxyHandler(jerseyApplication); - // for Spark applications, use the SparkLambdaContainerHandler.getAwsProxyHandler() helper method; + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + return handler.proxy(awsProxyRequest, context); + } +} +``` + +### Spring support +The library supports Spring applications that are configured using annotations (in code) rather than in an XML file. The simplest possible configuration uses the `@ComponentScan` annotation to load all controller classes from a package. For example, our unit test application has the following configuuration class. +```java +@Configuration +@ComponentScan("com.amazonaws.serverless.proxy.spring.echoapp") +public class EchoSpringAppConfig { +} +``` + +Once you have declared a configuration class, you can initialize the library with the class name: +```java +public class LambdaHandler implements RequestHandler { + SpringLambdacontainerHandler handler = + SpringLambdacontainerHandler.getAwsProxyHandler(EchoSpringAppConfig.class) + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { return handler.proxy(awsProxyRequest, context); } 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 42bac72b8..2b32410ad 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 @@ -62,7 +62,7 @@ protected LambdaContainerHandler(RequestReader { */ protected abstract ResponseType writeResponse(ContainerResponseType containerResponse, Context lambdaContext) throws InvalidResponseObjectException; + + /** + * Checks whether the given byte array contains a UTF-8 encoded string + * @param input The byte[] to check against + * @return true if the contend is valid UTF-8, false otherwise + */ + protected boolean isValidUtf8(final byte[] input) { + int i = 0; + // Check for BOM + if (input.length >= 3 && (input[0] & 0xFF) == 0xEF + && (input[1] & 0xFF) == 0xBB & (input[2] & 0xFF) == 0xBF) { + i = 3; + } + + int end; + for (int j = input.length; i < j; ++i) { + int octet = input[i]; + if ((octet & 0x80) == 0) { + continue; // ASCII + } + + // Check for UTF-8 leading byte + if ((octet & 0xE0) == 0xC0) { + end = i + 1; + } else if ((octet & 0xF0) == 0xE0) { + end = i + 2; + } else if ((octet & 0xF8) == 0xF0) { + end = i + 3; + } else { + // Java only supports BMP so 3 is max + return false; + } + + while (i < end) { + i++; + octet = input[i]; + if ((octet & 0xC0) != 0x80) { + // Not a valid trailing byte + return false; + } + } + } + return true; + } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/ApiGatewayAuthorizerContext.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/ApiGatewayAuthorizerContext.java index f79a1efde..294e96de8 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/ApiGatewayAuthorizerContext.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/ApiGatewayAuthorizerContext.java @@ -12,28 +12,27 @@ */ package com.amazonaws.serverless.proxy.internal.model; +import java.util.HashMap; + /** * Custom authorizer context object for the API Gateway request context. */ -public class ApiGatewayAuthorizerContext { - - //------------------------------------------------------------- - // Variables - Private - //------------------------------------------------------------- - - private String principalId; - +public class ApiGatewayAuthorizerContext extends HashMap { //------------------------------------------------------------- // Methods - Getter/Setter //------------------------------------------------------------- public String getPrincipalId() { - return principalId; + return get("principalId"); } public void setPrincipalId(String principalId) { - this.principalId = principalId; + put("principalId", principalId); + } + + public String getContextValue(String key) { + return get(key); } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/AwsProxyResponse.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/AwsProxyResponse.java index a72983559..43393fda9 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/AwsProxyResponse.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/model/AwsProxyResponse.java @@ -27,6 +27,7 @@ public class AwsProxyResponse { private int statusCode; private Map headers; private String body; + private boolean isBase64Encoded; //------------------------------------------------------------- @@ -100,4 +101,12 @@ public String getBody() { public void setBody(String body) { this.body = body; } + + public boolean isBase64Encoded() { + return isBase64Encoded; + } + + public void setBase64Encoded(boolean base64Encoded) { + isBase64Encoded = base64Encoded; + } } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java index b68ab06e8..a6d1ba07f 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsHttpServletResponse.java @@ -364,10 +364,17 @@ public Locale getLocale() { // Methods - Package //------------------------------------------------------------- - String getAwsResponseBody() { + String getAwsResponseBodyString() { return responseBody; } + byte[] getAwsResponseBodyBytes() { + if (bodyOutputStream != null) { + return bodyOutputStream.toByteArray(); + } + return new byte[0]; + } + Map getAwsResponseHeaders() { Map responseHeaders = new HashMap<>(); diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java index 59c2b0acc..44aa9f790 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequest.java @@ -180,6 +180,9 @@ public Enumeration getHeaders(String s) { @Override public Enumeration getHeaderNames() { + if (request.getHeaders() == null) { + return Collections.emptyEnumeration(); + } return Collections.enumeration(request.getHeaders().keySet()); } @@ -220,7 +223,7 @@ public String getPathTranslated() { @Override public String getContextPath() { - return request.getResource(); + return "/"; } @@ -529,7 +532,9 @@ public Map getParameterMap() { } for (Map.Entry> entry : params.entrySet()) { - output.put(entry.getKey(), (String[])entry.getValue().toArray()); + String[] valuesArray = new String[entry.getValue().size()]; + valuesArray = entry.getValue().toArray(valuesArray); + output.put(entry.getKey(), valuesArray); } return output; } diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java index bdfaa3462..3358e2705 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/servlet/AwsProxyHttpServletRequestReader.java @@ -32,7 +32,11 @@ public class AwsProxyHttpServletRequestReader extends RequestReaderAwsProxyResponse object given an AwsHttpServletResponse object. If the * response is not populated with a status code we infer a default 200 status code. @@ -32,7 +35,18 @@ public class AwsProxyHttpServletResponseWriter extends ResponseWriter()); this.request.setRequestContext(new ApiGatewayRequestContext()); - this.request.getRequestContext().setIdentity(new ApiGatewayRequestIdentity()); + ApiGatewayRequestIdentity identity = new ApiGatewayRequestIdentity(); + identity.setSourceIp("127.0.0.1"); + this.request.getRequestContext().setIdentity(identity); } @@ -120,11 +122,21 @@ public AwsProxyRequestBuilder body(String body) { public AwsProxyRequestBuilder authorizerPrincipal(String principal) { - this.request.getRequestContext().setAuthorizer(new ApiGatewayAuthorizerContext()); + if (this.request.getRequestContext().getAuthorizer() == null) { + this.request.getRequestContext().setAuthorizer(new ApiGatewayAuthorizerContext()); + } this.request.getRequestContext().getAuthorizer().setPrincipalId(principal); return this; } + public AwsProxyRequestBuilder authorizerContextValue(String key, String value) { + if (this.request.getRequestContext().getAuthorizer() == null) { + this.request.getRequestContext().setAuthorizer(new ApiGatewayAuthorizerContext()); + } + this.request.getRequestContext().getAuthorizer().put(key, value); + return this; + } + public AwsProxyRequestBuilder cognitoUserPool(String identityId) { this.request.getRequestContext().getIdentity().setCognitoAuthenticationType("POOL"); diff --git a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyResponseWriter.java b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyResponseWriter.java index 347120bf6..80ea48fac 100644 --- a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyResponseWriter.java +++ b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyAwsProxyResponseWriter.java @@ -59,6 +59,7 @@ public AwsProxyResponse writeResponse(JerseyResponseWriter containerResponse, Co responseString = new String(containerResponse.getResponseBody().toByteArray()); } else { responseString = Base64.getMimeEncoder().encodeToString(containerResponse.getResponseBody().toByteArray()); + response.setBase64Encoded(true); } response.setBody(responseString); @@ -69,53 +70,4 @@ public AwsProxyResponse writeResponse(JerseyResponseWriter containerResponse, Co throw new InvalidResponseObjectException(ex.getMessage(), ex); } } - - - //------------------------------------------------------------- - // Methods - Private - //------------------------------------------------------------- - - /** - * Checks whether the given byte array contains a UTF-8 encoded string - * @param input The byte[] to check against - * @return true if the contend is valid UTF-8, false otherwise - */ - private static boolean isValidUtf8(final byte[] input) { - int i = 0; - // Check for BOM - if (input.length >= 3 && (input[0] & 0xFF) == 0xEF - && (input[1] & 0xFF) == 0xBB & (input[2] & 0xFF) == 0xBF) { - i = 3; - } - - int end; - for (int j = input.length; i < j; ++i) { - int octet = input[i]; - if ((octet & 0x80) == 0) { - continue; // ASCII - } - - // Check for UTF-8 leading byte - if ((octet & 0xE0) == 0xC0) { - end = i + 1; - } else if ((octet & 0xF0) == 0xE0) { - end = i + 2; - } else if ((octet & 0xF8) == 0xF0) { - end = i + 3; - } else { - // Java only supports BMP so 3 is max - return false; - } - - while (i < end) { - i++; - octet = input[i]; - if ((octet & 0xC0) != 0x80) { - // Not a valid trailing byte - return false; - } - } - } - return true; - } } diff --git a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java index d98ae9695..ad3a0a0ca 100644 --- a/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java +++ b/aws-serverless-java-container-jersey/src/main/java/com/amazonaws/serverless/proxy/jersey/JerseyLambdaContainerHandler.java @@ -23,6 +23,7 @@ import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest; import com.amazonaws.serverless.proxy.internal.model.AwsProxyResponse; +import com.amazonaws.services.lambda.runtime.Context; import org.glassfish.jersey.server.ApplicationHandler; import org.glassfish.jersey.server.ContainerRequest; import org.glassfish.jersey.server.ResourceConfig; @@ -181,7 +182,7 @@ protected JerseyResponseWriter getContainerResponse(CountDownLatch latch) { @Override - protected void handleRequest(ContainerRequest containerRequest, JerseyResponseWriter jerseyResponseWriter) { + protected void handleRequest(ContainerRequest containerRequest, JerseyResponseWriter jerseyResponseWriter, Context lambdaContext) { containerRequest.setWriter(jerseyResponseWriter); applicationHandler.handle(containerRequest); diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/EchoJerseyResource.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/EchoJerseyResource.java index 222a52b98..1715d6b63 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/EchoJerseyResource.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/EchoJerseyResource.java @@ -79,6 +79,17 @@ public SingleValueModel echoAuthorizerPrincipal(@Context ContainerRequestContext return valueModel; } + @Path("/authorizer-context") @GET + @Produces(MediaType.APPLICATION_JSON) + public SingleValueModel echoAuthorizerContext(@Context ContainerRequestContext context, @QueryParam("key") String key) { + SingleValueModel valueModel = new SingleValueModel(); + ApiGatewayRequestContext apiGatewayRequestContext = + (ApiGatewayRequestContext) context.getProperty(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + valueModel.setValue(apiGatewayRequestContext.getAuthorizer().getContextValue(key)); + + return valueModel; + } + @Path("/json-body") @POST @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) diff --git a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/JerseyAwsProxyTest.java b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/JerseyAwsProxyTest.java index 1e08c8c61..a467c0662 100644 --- a/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/JerseyAwsProxyTest.java +++ b/aws-serverless-java-container-jersey/src/test/java/com/amazonaws/serverless/proxy/test/jersey/JerseyAwsProxyTest.java @@ -117,6 +117,22 @@ public void authorizer_securityContext_customPrincipalSuccess() { validateSingleValueModel(output, AUTHORIZER_PRINCIPAL_ID); } + @Test + public void authorizer_securityContext_customAuthorizerContextSuccess() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/authorizer-context", "GET") + .json() + .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID) + .authorizerContextValue(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) + .queryString("key", CUSTOM_HEADER_KEY) + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertEquals("application/json", output.getHeaders().get("Content-Type")); + + validateSingleValueModel(output, CUSTOM_HEADER_VALUE); + } + @Test public void errors_unknownRoute_expect404() { AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/test33", "GET").build(); diff --git a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java index ec02f6f01..4cc8322b3 100644 --- a/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java +++ b/aws-serverless-java-container-spark/src/main/java/com/amazonaws/serverless/proxy/spark/SparkLambdaContainerHandler.java @@ -23,6 +23,7 @@ import com.amazonaws.serverless.proxy.spark.embeddedserver.LambdaEmbeddedServer; import com.amazonaws.serverless.proxy.spark.embeddedserver.LambdaEmbeddedServerFactory; +import com.amazonaws.services.lambda.runtime.Context; import spark.Service; import spark.Spark; import spark.embeddedserver.EmbeddedServers; @@ -145,7 +146,7 @@ protected AwsHttpServletResponse getContainerResponse(CountDownLatch latch) { @Override - protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, AwsHttpServletResponse httpServletResponse) + protected void handleRequest(AwsProxyHttpServletRequest httpServletRequest, AwsHttpServletResponse httpServletResponse, Context lambdaContext) throws Exception { if (embeddedServer == null) { embeddedServer = LambdaEmbeddedServerFactory.getServerInstance(); diff --git a/aws-serverless-java-container-spring/pom.xml b/aws-serverless-java-container-spring/pom.xml new file mode 100644 index 000000000..96f432fdc --- /dev/null +++ b/aws-serverless-java-container-spring/pom.xml @@ -0,0 +1,94 @@ + + + + com.amazonaws.serverless + aws-serverless-java-container-spring + 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/ + 0.1 + + + https://github.com/awslabs/aws-serverless-java-container.git + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + 4.0.0 + + + 1.8 + 1.8 + 4.3.4.RELEASE + 2.8.4 + + + + + + com.amazonaws.serverless + aws-serverless-java-container-core + 0.1 + + + + + org.springframework + spring-webmvc + ${spring.version} + + + + + org.springframework + spring-test + ${spring.version} + test + + + + + commons-codec + commons-codec + 1.10 + test + + + + + junit + junit + 4.12 + test + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + test + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + test + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + test + + + + + \ No newline at end of file diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/LambdaSpringApplicationInitializer.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/LambdaSpringApplicationInitializer.java new file mode 100644 index 000000000..82b82c294 --- /dev/null +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/LambdaSpringApplicationInitializer.java @@ -0,0 +1,160 @@ +/* + * 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 org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.ConfigurableWebApplicationContext; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.context.support.ServletRequestHandledEvent; +import org.springframework.web.servlet.DispatcherServlet; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +/** + * Custom implementation of Spring's `WebApplicationInitializer`. Uses internal variables to keep application state + * and creates a DispatcherServlet to handle incoming events. When the first event arrives it extracts the ServletContext + * and starts the Spring application. This class assumes that the implementation of `HttpServletRequest` that is passed + * in correctly implements the `getServletContext` method. + * + * State is kept using the `initialized` boolean variable. Each time a new event is received, the app sets the + * `currentResponse` private property to the value of the new `HttpServletResponse` object. This is used to intercept + * Spring notifications for the `ServletRequestHandledEvent` and call the flush method to release the latch. + */ +public class LambdaSpringApplicationInitializer implements WebApplicationInitializer { + public static final String ERROR_NO_CONTEXT = "No application context or configuration classes provided"; + + private static final String DEFAULT_SERVLET_NAME = "aws-servless-java-container"; + + // Configuration variables that can be passed in + private ConfigurableWebApplicationContext applicationContext; + private boolean refreshContext = true; + private List contextListeners; + + // Dynamically instantiated properties + private ServletConfig dispatcherConfig; + private DispatcherServlet dispatcherServlet; + + // The current response is used to release the latch when Spring emits the request handled event + private HttpServletResponse currentResponse; + + /** + * Creates a new instance of the WebApplicationInitializer + * @param applicationContext A custom ConfigurableWebApplicationContext to be used + */ + public LambdaSpringApplicationInitializer(ConfigurableWebApplicationContext applicationContext) { + this.contextListeners = new ArrayList<>(); + this.applicationContext = applicationContext; + } + + /** + * Adds a new listener for the servlet context events. At the moment the library only emits events when the application + * is initialized. Because we don't have container lifecycle notifications from Lambda the `contextDestroyed` + * method is never called + * @param listener An implementation of `ServletContextListener` + */ + public void addListener(ServletContextListener listener) { + contextListeners.add(listener); + } + + public void setRefreshContext(boolean refreshContext) { + this.refreshContext = refreshContext; + } + + public void dispatch(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + currentResponse = response; + dispatcherServlet.service(request, response); + } + + @Override + public void onStartup(ServletContext servletContext) throws ServletException { + applicationContext.setServletContext(servletContext); + + dispatcherConfig = new DefaultDispatcherConfig(servletContext); + applicationContext.setServletConfig(dispatcherConfig); + + // Configure the listener for the request handled events. All we do here is release the latch + applicationContext.addApplicationListener(new ApplicationListener() { + @Override + public void onApplicationEvent(ServletRequestHandledEvent servletRequestHandledEvent) { + try { + currentResponse.flushBuffer(); + } catch (IOException e) { + e.printStackTrace(); + throw new RuntimeException("Could not flush response buffer", e); + } + } + }); + + // Manage the lifecycle of the root application context + this.addListener(new ContextLoaderListener(applicationContext)); + + // Register and map the dispatcher servlet + dispatcherServlet = new DispatcherServlet(applicationContext); + + if (refreshContext) { + dispatcherServlet.refresh(); + } + + dispatcherServlet.onApplicationEvent(new ContextRefreshedEvent(applicationContext)); + dispatcherServlet.init(dispatcherConfig); + + notifyStartListeners(servletContext); + } + + private void notifyStartListeners(ServletContext context) { + for (ServletContextListener listener : contextListeners) { + listener.contextInitialized(new ServletContextEvent(context)); + } + } + + /** + * Default configuration class for the DispatcherServlet. This just mocks the behaviour of a default + * ServletConfig object with no init parameters + */ + private class DefaultDispatcherConfig implements ServletConfig { + private ServletContext servletContext; + + DefaultDispatcherConfig(ServletContext context) { + servletContext = context; + } + + @Override + public String getServletName() { + return DEFAULT_SERVLET_NAME; + } + + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public String getInitParameter(String s) { + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.emptyEnumeration(); + } + } +} diff --git a/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java new file mode 100644 index 000000000..ed3a1ba6e --- /dev/null +++ b/aws-serverless-java-container-spring/src/main/java/com/amazonaws/serverless/proxy/spring/SpringLambdaContainerHandler.java @@ -0,0 +1,122 @@ +/* + * 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 com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.*; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsHttpServletResponse; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequest; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletRequestReader; +import com.amazonaws.serverless.proxy.internal.servlet.AwsProxyHttpServletResponseWriter; +import com.amazonaws.services.lambda.runtime.Context; +import org.springframework.web.context.ConfigurableWebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; + +import javax.servlet.ServletContext; +import java.util.concurrent.CountDownLatch; + +/** + * Spring 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. + * @param The incoming event type + * @param The expected return type + */ +public class SpringLambdaContainerHandler extends LambdaContainerHandler { + private LambdaSpringApplicationInitializer initializer; + + // State vars + private boolean initialized; + + /** + * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `AwsProxyResponse` objects + * @param config A set of classes annotated with the Spring @Configuration annotation + * @return An initialized instance of the `SpringLambdaContainerHandler` + * @throws ContainerInitializationException + */ + public static SpringLambdaContainerHandler getAwsProxyHandler(Class... config) throws ContainerInitializationException { + AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); + applicationContext.register(config); + + return new SpringLambdaContainerHandler<>( + new AwsProxyHttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsProxySecurityContextWriter(), + new AwsProxyExceptionHandler(), + applicationContext + ); + } + + /** + * Creates a default SpringLambdaContainerHandler initialized with the `AwsProxyRequest` and `AwsProxyResponse` objects + * @param applicationContext A custom ConfigurableWebApplicationContext to be used + * @return An initialized instance of the `SpringLambdaContainerHandler` + * @throws ContainerInitializationException + */ + public static SpringLambdaContainerHandler getAwsProxyHandler(ConfigurableWebApplicationContext applicationContext) + throws ContainerInitializationException { + return new SpringLambdaContainerHandler<>( + new AwsProxyHttpServletRequestReader(), + new AwsProxyHttpServletResponseWriter(), + new AwsProxySecurityContextWriter(), + new AwsProxyExceptionHandler(), + applicationContext + ); + } + + /** + * Creates a new container handler with the given reader and writer objects + * + * @param requestReader An implementation of `RequestReader` + * @param responseWriter An implementation of `ResponseWriter` + * @param securityContextWriter An implementation of `SecurityContextWriter` + * @param exceptionHandler An implementation of `ExceptionHandler` + * @throws ContainerInitializationException + */ + public SpringLambdaContainerHandler(RequestReader requestReader, + ResponseWriter responseWriter, + SecurityContextWriter securityContextWriter, + ExceptionHandler exceptionHandler, + ConfigurableWebApplicationContext applicationContext) + throws ContainerInitializationException { + super(requestReader, responseWriter, securityContextWriter, exceptionHandler); + initializer = new LambdaSpringApplicationInitializer(applicationContext); + } + + public void setRefreshContext(boolean refreshContext) { + this.initializer.setRefreshContext(refreshContext); + } + + @Override + protected AwsHttpServletResponse getContainerResponse(CountDownLatch latch) { + return new AwsHttpServletResponse(latch); + } + + @Override + protected void handleRequest(AwsProxyHttpServletRequest containerRequest, AwsHttpServletResponse containerResponse, Context lambdaContext) throws Exception { + if (initializer == null) { + throw new ContainerInitializationException(LambdaSpringApplicationInitializer.ERROR_NO_CONTEXT, null); + } + + // wire up the application context on the first invocation + if (!initialized) { + ServletContext context = containerRequest.getServletContext(); + initializer.onStartup(context); + initialized = true; + } + + initializer.dispatch(containerRequest, containerResponse); + } +} 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 new file mode 100644 index 000000000..5683dec2f --- /dev/null +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/SpringAwsProxyTest.java @@ -0,0 +1,190 @@ +package com.amazonaws.serverless.proxy.spring; + +import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.spring.echoapp.EchoSpringAppConfig; +import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; +import com.amazonaws.serverless.proxy.spring.echoapp.model.SingleValueModel; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.codec.binary.Base64; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.TestExecutionListeners; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.io.IOException; +import java.util.UUID; + +import static org.junit.Assert.*; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(classes = {EchoSpringAppConfig.class}) +@WebAppConfiguration +@TestExecutionListeners(inheritListeners = false, listeners = {DependencyInjectionTestExecutionListener.class}) +public class SpringAwsProxyTest { + private static final String CUSTOM_HEADER_KEY = "x-custom-header"; + private static final String CUSTOM_HEADER_VALUE = "my-custom-value"; + private static final String AUTHORIZER_PRINCIPAL_ID = "test-principal-" + UUID.randomUUID().toString(); + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private MockLambdaContext lambdaContext; + + @Autowired + private SpringLambdaContainerHandler handler; + + @Test + public void headers_getHeaders_echo() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/headers", "GET") + .json() + .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertEquals("application/json", output.getHeaders().get("Content-Type").split(";")[0]); + + validateMapResponseModel(output); + } + + @Test + public void headers_servletRequest_echo() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/servlet-headers", "GET") + .json() + .header(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertEquals("application/json", output.getHeaders().get("Content-Type").split(";")[0]); + + validateMapResponseModel(output); + } + + @Test + public void queryString_uriInfo_echo() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/query-string", "GET") + .json() + .queryString(CUSTOM_HEADER_KEY, CUSTOM_HEADER_VALUE) + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertEquals("application/json", output.getHeaders().get("Content-Type").split(";")[0]); + + validateMapResponseModel(output); + } + + @Test + public void authorizer_securityContext_customPrincipalSuccess() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/authorizer-principal", "GET") + .json() + .authorizerPrincipal(AUTHORIZER_PRINCIPAL_ID) + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertEquals("application/json", output.getHeaders().get("Content-Type").split(";")[0]); + + validateSingleValueModel(output, AUTHORIZER_PRINCIPAL_ID); + } + + @Test + public void errors_unknownRoute_expect404() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/test33", "GET").build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(404, output.getStatusCode()); + } + + @Test + public void error_contentType_invalidContentType() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/json-body", "POST") + .header("Content-Type", "application/octet-stream") + .body("asdasdasd") + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(415, output.getStatusCode()); + } + + @Test + public void error_statusCode_methodNotAllowed() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/status-code", "POST") + .json() + .queryString("status", "201") + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(405, output.getStatusCode()); + } + + @Test + public void responseBody_responseWriter_validBody() throws JsonProcessingException { + SingleValueModel singleValueModel = new SingleValueModel(); + singleValueModel.setValue(CUSTOM_HEADER_VALUE); + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/json-body", "POST") + .json() + .header("Content-Type", "application/json") + .body(objectMapper.writeValueAsString(singleValueModel)) + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(200, output.getStatusCode()); + assertNotNull(output.getBody()); + System.out.println("Output:" + output.getBody()); + validateSingleValueModel(output, CUSTOM_HEADER_VALUE); + } + + @Test + public void statusCode_responseStatusCode_customStatusCode() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/status-code", "GET") + .json() + .queryString("status", "201") + .build(); + + AwsProxyResponse output = handler.proxy(request, lambdaContext); + assertEquals(201, output.getStatusCode()); + } + + @Test + public void base64_binaryResponse_base64Encoding() { + AwsProxyRequest request = new AwsProxyRequestBuilder("/echo/binary", "GET").build(); + + AwsProxyResponse response = handler.proxy(request, lambdaContext); + assertNotNull(response.getBody()); + assertTrue(Base64.isBase64(response.getBody())); + } + + private void validateMapResponseModel(AwsProxyResponse output) { + try { + 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) { + fail("Exception while parsing response body: " + e.getMessage()); + e.printStackTrace(); + } + } + + private void validateSingleValueModel(AwsProxyResponse output, String value) { + try { + SingleValueModel response = objectMapper.readValue(output.getBody(), SingleValueModel.class); + assertNotNull(response.getValue()); + assertEquals(value, response.getValue()); + } catch (IOException e) { + fail("Exception while parsing response body: " + e.getMessage()); + e.printStackTrace(); + } + } +} + diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoResource.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoResource.java new file mode 100644 index 000000000..27e18e0c4 --- /dev/null +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoResource.java @@ -0,0 +1,86 @@ +package com.amazonaws.serverless.proxy.spring.echoapp; + +import com.amazonaws.serverless.proxy.internal.RequestReader; +import com.amazonaws.serverless.proxy.internal.model.ApiGatewayRequestContext; +import com.amazonaws.serverless.proxy.spring.echoapp.model.MapResponseModel; +import com.amazonaws.serverless.proxy.spring.echoapp.model.SingleValueModel; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import javax.servlet.http.HttpServletRequest; +import java.util.Enumeration; +import java.util.Map; +import java.util.Random; + +@RestController +@EnableWebMvc +@RequestMapping("/echo") +public class EchoResource { + @RequestMapping(path = "/headers", method = RequestMethod.GET) + public MapResponseModel echoHeaders(@RequestHeader Map allHeaders) { + MapResponseModel headers = new MapResponseModel(); + for (String key : allHeaders.keySet()) { + headers.addValue(key, allHeaders.get(key)); + } + + return headers; + } + + @RequestMapping(path = "/servlet-headers", method = RequestMethod.GET) + public MapResponseModel echoServletHeaders(HttpServletRequest context) { + MapResponseModel headers = new MapResponseModel(); + Enumeration headerNames = context.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String headerName = headerNames.nextElement(); + headers.addValue(headerName, context.getHeader(headerName)); + } + + return headers; + } + + @RequestMapping(path = "/query-string", method = RequestMethod.GET) + public MapResponseModel echoQueryString(HttpServletRequest request) { + MapResponseModel queryStrings = new MapResponseModel(); + for (String key : request.getParameterMap().keySet()) { + queryStrings.addValue(key, request.getParameterMap().get(key)[0]); + } + + return queryStrings; + } + + @RequestMapping(path = "/authorizer-principal", method = RequestMethod.GET) + public SingleValueModel echoAuthorizerPrincipal(HttpServletRequest context) { + SingleValueModel valueModel = new SingleValueModel(); + ApiGatewayRequestContext apiGatewayRequestContext = + (ApiGatewayRequestContext) context.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + valueModel.setValue(apiGatewayRequestContext.getAuthorizer().getPrincipalId()); + + return valueModel; + } + + @RequestMapping(path = "/json-body", method = RequestMethod.POST, consumes = { "application/json" }) + public SingleValueModel echoJsonValue(@RequestBody SingleValueModel requestValue) { + SingleValueModel output = new SingleValueModel(); + output.setValue(requestValue.getValue()); + + return output; + } + + @RequestMapping(path = "/status-code", method = RequestMethod.GET) + public ResponseEntity echoCustomStatusCode(@RequestParam("status") int statusCode ) { + SingleValueModel output = new SingleValueModel(); + output.setValue("" + statusCode); + + return new ResponseEntity(output, HttpStatus.valueOf(statusCode)); + } + + @RequestMapping(path = "/binary", method = RequestMethod.GET) + public ResponseEntity echoBinaryData() { + byte[] b = new byte[128]; + new Random().nextBytes(b); + + return new ResponseEntity(b, HttpStatus.OK); + } +} 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 new file mode 100644 index 000000000..53ace4766 --- /dev/null +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/EchoSpringAppConfig.java @@ -0,0 +1,39 @@ +package com.amazonaws.serverless.proxy.spring.echoapp; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.spring.LambdaSpringApplicationInitializer; +import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.context.ConfigurableWebApplicationContext; + +@Configuration +@ComponentScan("com.amazonaws.serverless.proxy.spring.echoapp") +public class EchoSpringAppConfig { + + @Autowired + private ConfigurableWebApplicationContext applicationContext; + + @Bean + public SpringLambdaContainerHandler springLambdaContainerHandler() throws ContainerInitializationException { + SpringLambdaContainerHandler handler = SpringLambdaContainerHandler.getAwsProxyHandler(applicationContext); + handler.setRefreshContext(false); + return handler; + } + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean + public MockLambdaContext lambdaContext() { + return new MockLambdaContext(); + } + +} diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/model/MapResponseModel.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/model/MapResponseModel.java new file mode 100644 index 000000000..326f7138d --- /dev/null +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/model/MapResponseModel.java @@ -0,0 +1,39 @@ +/* + * 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.echoapp.model; + +import java.util.HashMap; +import java.util.Map; + +/** + * Request/response model + */ +public class MapResponseModel { + private Map values; + + public MapResponseModel() { + this.values = new HashMap<>(); + } + + public void addValue(String key, String value) { + this.values.put(key, value); + } + + public Map getValues() { + return values; + } + + public void setValues(Map values) { + this.values = values; + } +} \ No newline at end of file diff --git a/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/model/SingleValueModel.java b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/model/SingleValueModel.java new file mode 100644 index 000000000..0d04f585b --- /dev/null +++ b/aws-serverless-java-container-spring/src/test/java/com/amazonaws/serverless/proxy/spring/echoapp/model/SingleValueModel.java @@ -0,0 +1,28 @@ +/* + * 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.echoapp.model; + +/** + * Request/response model + */ +public class SingleValueModel { + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index d272ed668..f93aae29f 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ aws-serverless-java-container-core aws-serverless-java-container-jersey aws-serverless-java-container-spark + aws-serverless-java-container-spring diff --git a/samples/jersey/pet-store/README.md b/samples/jersey/pet-store/README.md new file mode 100644 index 000000000..bfb441648 --- /dev/null +++ b/samples/jersey/pet-store/README.md @@ -0,0 +1,62 @@ +# Serverless Jersey example +A basic pet store written with the [Jersey framework](https://jersey.java.net/). The `LambdaHandler` 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 `sam.yaml` file in the root folder contains the application definition + +## Installation +To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. + +In a shell, navigate to the sample's folder and use maven to build a deployable jar. +``` +$ mvn package +``` + +This command should generate a `serverless-jersey-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. + +You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: + +``` +$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket +Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) +Successfully packaged artifacts and wrote output template to file output-sam.yaml. +Execute the following command to deploy the packaged template +aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +``` + +As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. + +``` +$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessJerseySample --capabilities CAPABILITY_IAM +``` + +Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `JerseyPetStoreApi` key of the `Outputs` property: + +``` +$ aws cloudformation describe-stacks --stack-name ServerlessJerseySample +{ + "Stacks": [ + { + "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", + "Description": "Example Pet Store API written in jersey with the aws-serverless-java-container library", + "Tags": [], + "Outputs": [ + { + "Description": "URL for application", + "OutputKey": "JerseyPetStoreApi", + "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" + } + ], + "CreationTime": "2016-12-13T22:59:31.552Z", + "Capabilities": [ + "CAPABILITY_IAM" + ], + "StackName": "JerseySample", + "NotificationARNs": [], + "StackStatus": "UPDATE_COMPLETE" + } + ] +} + +``` + +Copy the `OutputValue` into a browser to test a first request. \ No newline at end of file diff --git a/samples/jersey/pet-store/pom.xml b/samples/jersey/pet-store/pom.xml new file mode 100644 index 000000000..edacdd2a2 --- /dev/null +++ b/samples/jersey/pet-store/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-jersey-example + 1.0-SNAPSHOT + Jersey example for the aws-serverless-java-container library + Simple pet store written in Jersey + https://aws.amazon.com/lambda/ + + + https://github.com/awslabs/aws-serverless-java-container.git + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 1.8 + 1.8 + 2.24 + 2.8.5 + + + + + com.amazonaws.serverless + aws-serverless-java-container-jersey + 0.1 + + + + com.amazonaws + aws-lambda-java-core + 1.1.0 + + + + org.glassfish.jersey.media + jersey-media-json-jackson + ${jersey.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + + + \ No newline at end of file diff --git a/samples/jersey/pet-store/sam.yaml b/samples/jersey/pet-store/sam.yaml new file mode 100644 index 000000000..4b973809a --- /dev/null +++ b/samples/jersey/pet-store/sam.yaml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written in jersey with the aws-serverless-java-container library +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.jersey.LambdaHandler::handleRequest + Runtime: java8 + CodeUri: target/serverless-jersey-example-1.0-SNAPSHOT.jar + MemorySize: 512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 20 + Events: + GetResource: + Type: Api + Properties: + Path: /{proxy+} + Method: any + +Outputs: + JerseyPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Export: + Name: JerseyPetStoreApi diff --git a/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/LambdaHandler.java b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/LambdaHandler.java new file mode 100644 index 000000000..8d67d2991 --- /dev/null +++ b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/LambdaHandler.java @@ -0,0 +1,34 @@ +/* + * 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.jersey; + +import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.jersey.JerseyLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import org.glassfish.jersey.jackson.JacksonFeature; +import org.glassfish.jersey.server.ResourceConfig; + +public class LambdaHandler implements RequestHandler { + private ResourceConfig jerseyApplication = new ResourceConfig() + .packages("com.amazonaws.serverless.sample.jersey") + .register(JacksonFeature.class); + private JerseyLambdaContainerHandler handler + = JerseyLambdaContainerHandler.getAwsProxyHandler(jerseyApplication); + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + return handler.proxy(awsProxyRequest, context); + } +} \ No newline at end of file diff --git a/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/PetsResource.java b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/PetsResource.java new file mode 100644 index 000000000..2d92510d7 --- /dev/null +++ b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/PetsResource.java @@ -0,0 +1,70 @@ +/* + * 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.jersey; + +import com.amazonaws.serverless.sample.jersey.model.Pet; +import com.amazonaws.serverless.sample.jersey.model.PetData; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.UUID; + +@Path("/pets") +public class PetsResource { + + @POST + @Produces(MediaType.APPLICATION_JSON) + @Consumes(MediaType.APPLICATION_JSON) + public Response createPet(final Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return Response.status(400).entity(new Error("Invalid name or breed")).build(); + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return Response.status(200).entity(dbPet).build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + public Pet[] listPets(@QueryParam("limit") int limit) { + if (limit < 1) { + limit = 10; + } + + Pet[] outputPets = new Pet[limit]; + + for (int i = 0; i < limit; 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; + } + + @Path("/{petId}") @GET + @Produces(MediaType.APPLICATION_JSON) + public Pet getPetDetails() { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } +} \ No newline at end of file diff --git a/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/Error.java b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/Error.java new file mode 100644 index 000000000..b6ca51891 --- /dev/null +++ b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/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.jersey.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/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/Pet.java b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/Pet.java new file mode 100644 index 000000000..2a1a220f8 --- /dev/null +++ b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/Pet.java @@ -0,0 +1,54 @@ +/* + * 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.jersey.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/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/PetData.java b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/PetData.java new file mode 100644 index 000000000..fb1ee15c7 --- /dev/null +++ b/samples/jersey/pet-store/src/main/java/com/amazonaws/serverless/sample/jersey/model/PetData.java @@ -0,0 +1,111 @@ +/* + * 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.jersey.model; + +import java.util.*; +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/spark/pet-store/README.md b/samples/spark/pet-store/README.md new file mode 100644 index 000000000..307e74ece --- /dev/null +++ b/samples/spark/pet-store/README.md @@ -0,0 +1,62 @@ +# Serverless Jersey example +A basic pet store written with the [Spark framework](http://sparkjava.com/). The `LambdaHandler` 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 `sam.yaml` file in the root folder contains the application definition + +## Installation +To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. + +In a shell, navigate to the sample's folder and use maven to build a deployable jar. +``` +$ mvn package +``` + +This command should generate a `serverless-spark-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. + +You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: + +``` +$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket +Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) +Successfully packaged artifacts and wrote output template to file output-sam.yaml. +Execute the following command to deploy the packaged template +aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +``` + +As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. + +``` +$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSparkSample --capabilities CAPABILITY_IAM +``` + +Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `SparkPetStoreApi` key of the `Outputs` property: + +``` +$ aws cloudformation describe-stacks --stack-name ServerlessSparkSample +{ + "Stacks": [ + { + "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", + "Description": "Example Pet Store API written with spark with the aws-serverless-java-container library", + "Tags": [], + "Outputs": [ + { + "Description": "URL for application", + "OutputKey": "PetStoreApi", + "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" + } + ], + "CreationTime": "2016-12-13T22:59:31.552Z", + "Capabilities": [ + "CAPABILITY_IAM" + ], + "StackName": "JerseySample", + "NotificationARNs": [], + "StackStatus": "UPDATE_COMPLETE" + } + ] +} + +``` + +Copy the `OutputValue` into a browser to test a first request. \ No newline at end of file diff --git a/samples/spark/pet-store/pom.xml b/samples/spark/pet-store/pom.xml new file mode 100644 index 000000000..16f0f849e --- /dev/null +++ b/samples/spark/pet-store/pom.xml @@ -0,0 +1,114 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-spark-example + 1.0-SNAPSHOT + Spark example for the aws-serverless-java-container library + Simple pet store written with the Spark framework + https://aws.amazon.com/lambda/ + + + https://github.com/awslabs/aws-serverless-java-container.git + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 1.8 + 1.8 + 2.8.5 + 2.5.3 + + + + + com.amazonaws.serverless + aws-serverless-java-container-spark + 0.1 + + + + com.amazonaws + aws-lambda-java-core + 1.1.0 + + + + com.amazonaws + aws-lambda-java-log4j + 1.0.0 + + + + + com.sparkjava + spark-core + ${spark.version} + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + org.slf4j + slf4j-log4j12 + 1.7.21 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + org.eclipse.jetty.websocket:* + + + + + + + + + \ No newline at end of file diff --git a/samples/spark/pet-store/sam.yaml b/samples/spark/pet-store/sam.yaml new file mode 100644 index 000000000..84a853686 --- /dev/null +++ b/samples/spark/pet-store/sam.yaml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with spark with the aws-serverless-java-container library +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.spark.LambdaHandler::handleRequest + Runtime: java8 + CodeUri: target/serverless-spark-example-1.0-SNAPSHOT.jar + MemorySize: 512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 20 + Events: + GetResource: + Type: Api + Properties: + Path: /{proxy+} + Method: any + +Outputs: + SparkPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Export: + Name: SparkPetStoreApi diff --git a/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/JsonTransformer.java b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/JsonTransformer.java new file mode 100644 index 000000000..55e5e5bec --- /dev/null +++ b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/JsonTransformer.java @@ -0,0 +1,32 @@ +/* + * 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.spark; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import spark.ResponseTransformer; + +public class JsonTransformer implements ResponseTransformer { + + private ObjectMapper mapper = new ObjectMapper(); + + @Override + public String render(Object model) { + try { + return mapper.writeValueAsString(model); + } catch (JsonProcessingException e) { + return null; + } + } + +} \ No newline at end of file diff --git a/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/LambdaHandler.java b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/LambdaHandler.java new file mode 100644 index 000000000..08ecc151d --- /dev/null +++ b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/LambdaHandler.java @@ -0,0 +1,102 @@ +/* + * 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.spark; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spark.SparkLambdaContainerHandler; +import com.amazonaws.serverless.sample.spark.model.Pet; +import com.amazonaws.serverless.sample.spark.model.PetData; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.log4j.LambdaAppender; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.log4j.Logger; + +import javax.ws.rs.core.Response; +import java.util.UUID; + +import static spark.Spark.before; +import static spark.Spark.get; +import static spark.Spark.post; + +public class LambdaHandler implements RequestHandler { + private ObjectMapper objectMapper = new ObjectMapper(); + private boolean isInitialized = false; + private SparkLambdaContainerHandler handler; + + @Override + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + if (!isInitialized) { + isInitialized = true; + try { + Logger.getRootLogger().addAppender(new LambdaAppender()); + handler = SparkLambdaContainerHandler.getAwsProxyHandler(); + defineResources(); + } catch (ContainerInitializationException e) { + e.printStackTrace(); + return null; + } + } + return handler.proxy(awsProxyRequest, context); + } + + private void defineResources() { + before((request, response) -> response.type("application/json")); + + post("/pets", (req, res) -> { + Pet newPet = objectMapper.readValue(req.body(), Pet.class); + if (newPet.getName() == null || newPet.getBreed() == null) { + return Response.status(400).entity(new Error("Invalid name or breed")).build(); + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + + res.status(200); + return dbPet; + }, new JsonTransformer()); + + get("/pets", (req, res) -> { + int limit = 10; + if (req.queryParams("limit") != null) { + limit = Integer.parseInt(req.queryParams("limit")); + } + + Pet[] outputPets = new Pet[limit]; + + for (int i = 0; i < limit; 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; + } + + res.status(200); + return outputPets; + }, new JsonTransformer()); + + get("/pets/:petId", (req, res) -> { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + res.status(200); + return newPet; + }, new JsonTransformer()); + } +} \ No newline at end of file diff --git a/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/Error.java b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/Error.java new file mode 100644 index 000000000..3577e6a3a --- /dev/null +++ b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/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.spark.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/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/Pet.java b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/Pet.java new file mode 100644 index 000000000..a6f569597 --- /dev/null +++ b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/Pet.java @@ -0,0 +1,54 @@ +/* + * 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.spark.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/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/PetData.java b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/PetData.java new file mode 100644 index 000000000..a8c45a53a --- /dev/null +++ b/samples/spark/pet-store/src/main/java/com/amazonaws/serverless/sample/spark/model/PetData.java @@ -0,0 +1,111 @@ +/* + * 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.spark.model; + +import java.util.*; +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/spring/pet-store/README.md b/samples/spring/pet-store/README.md new file mode 100644 index 000000000..44db0eacb --- /dev/null +++ b/samples/spring/pet-store/README.md @@ -0,0 +1,62 @@ +# Serverless Jersey example +A basic pet store written with the [Spring framework](https://projects.spring.io/spring-framework/). The `LambdaHandler` 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 `sam.yaml` file in the root folder contains the application definition + +## Installation +To build and install the sample application you will need [Maven](https://maven.apache.org/) and the [AWS CLI](https://aws.amazon.com/cli/) installed on your computer. + +In a shell, navigate to the sample's folder and use maven to build a deployable jar. +``` +$ mvn package +``` + +This command should generate a `serverless-spring-example-1.0-SNAPSHOT.jar` in the `target` folder. Now that we have generated the jar file, we can use the AWS CLI to package the template for deployment. + +You will need an S3 bucket to store the artifacts for deployment. Once you have created the S3 bucket, run the following command from the sample's folder: + +``` +$ aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket +Uploading to xxxxxxxxxxxxxxxxxxxxxxxxxx 6464692 / 6464692.0 (100.00%) +Successfully packaged artifacts and wrote output template to file output-sam.yaml. +Execute the following command to deploy the packaged template +aws cloudformation deploy --template-file /your/path/output-sam.yaml --stack-name +``` + +As the command output suggests, you can now use the cli to deploy the application. Choose a stack name and run the `aws cloudformation deploy` command from the output of the package command. + +``` +$ aws cloudformation deploy --template-file output-sam.yaml --stack-name ServerlessSpringSample --capabilities CAPABILITY_IAM +``` + +Once the application is deployed, you can describe the stack to show the API endpoint that was created. The endpoint should be the `SparkPetStoreApi` key of the `Outputs` property: + +``` +$ aws cloudformation describe-stacks --stack-name ServerlessSpringSample +{ + "Stacks": [ + { + "StackId": "arn:aws:cloudformation:us-west-2:xxxxxxxx:stack/JerseySample/xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx", + "Description": "Example Pet Store API written with spark with the aws-serverless-java-container library", + "Tags": [], + "Outputs": [ + { + "Description": "URL for application", + "OutputKey": "PetStoreApi", + "OutputValue": "https://xxxxxxx.execute-api.us-west-2.amazonaws.com/Prod/pets" + } + ], + "CreationTime": "2016-12-13T22:59:31.552Z", + "Capabilities": [ + "CAPABILITY_IAM" + ], + "StackName": "JerseySample", + "NotificationARNs": [], + "StackStatus": "UPDATE_COMPLETE" + } + ] +} + +``` + +Copy the `OutputValue` into a browser to test a first request. \ No newline at end of file diff --git a/samples/spring/pet-store/pom.xml b/samples/spring/pet-store/pom.xml new file mode 100644 index 000000000..8d2c8d014 --- /dev/null +++ b/samples/spring/pet-store/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + serverless-spring-example + 1.0-SNAPSHOT + Spring example for the aws-serverless-java-container library + Simple pet store written with the Spring framework + https://aws.amazon.com/lambda/ + + + https://github.com/awslabs/aws-serverless-java-container.git + + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + 1.8 + 1.8 + 4.3.4.RELEASE + 2.8.4 + + + + + com.amazonaws.serverless + aws-serverless-java-container-spring + 0.1 + + + + com.amazonaws + aws-lambda-java-core + 1.1.0 + + + + + org.springframework + spring-webmvc + ${spring.version} + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + + + \ No newline at end of file diff --git a/samples/spring/pet-store/sam.yaml b/samples/spring/pet-store/sam.yaml new file mode 100644 index 000000000..82d85dbc7 --- /dev/null +++ b/samples/spring/pet-store/sam.yaml @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with spring with the aws-serverless-java-container library +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.spring.LambdaHandler::handleRequest + Runtime: java8 + CodeUri: target/serverless-spring-example-1.0-SNAPSHOT.jar + MemorySize: 512 + Policies: AWSLambdaBasicExecutionRole + Timeout: 20 + Events: + GetResource: + Type: Api + Properties: + Path: /{proxy+} + Method: any + +Outputs: + SpringPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/pets' + Export: + Name: SpringPetStoreApi diff --git a/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/LambdaHandler.java b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/LambdaHandler.java new file mode 100644 index 000000000..d52fc8a5d --- /dev/null +++ b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/LambdaHandler.java @@ -0,0 +1,41 @@ +/* + * 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.spring; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.internal.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +/** + * Created by bulianis on 12/13/16. + */ +public class LambdaHandler implements RequestHandler { + SpringLambdaContainerHandler handler; + boolean isinitialized = false; + + public AwsProxyResponse handleRequest(AwsProxyRequest awsProxyRequest, Context context) { + if (!isinitialized) { + isinitialized = true; + try { + handler = SpringLambdaContainerHandler.getAwsProxyHandler(PetStoreSpringAppConfig.class); + } catch (ContainerInitializationException e) { + e.printStackTrace(); + return null; + } + } + return handler.proxy(awsProxyRequest, context); + } +} diff --git a/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/PetStoreSpringAppConfig.java b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/PetStoreSpringAppConfig.java new file mode 100644 index 000000000..5884d170c --- /dev/null +++ b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/PetStoreSpringAppConfig.java @@ -0,0 +1,21 @@ +/* + * 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.spring; + +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ComponentScan("com.amazonaws.serverless.sample.spring") +public class PetStoreSpringAppConfig { +} diff --git a/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/PetsController.java b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/PetsController.java new file mode 100644 index 000000000..9d866041d --- /dev/null +++ b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/PetsController.java @@ -0,0 +1,68 @@ +/* + * 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.spring; + +import com.amazonaws.serverless.sample.spring.model.Pet; +import com.amazonaws.serverless.sample.spring.model.PetData; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +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) { + 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/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/Error.java b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/Error.java new file mode 100644 index 000000000..1c7a96ce7 --- /dev/null +++ b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/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.spring.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/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/Pet.java b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/Pet.java new file mode 100644 index 000000000..124351cfd --- /dev/null +++ b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/Pet.java @@ -0,0 +1,54 @@ +/* + * 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.spring.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/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/PetData.java b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/PetData.java new file mode 100644 index 000000000..b06348d91 --- /dev/null +++ b/samples/spring/pet-store/src/main/java/com/amazonaws/serverless/sample/spring/model/PetData.java @@ -0,0 +1,111 @@ +/* + * 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.spring.model; + +import java.util.*; +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(); + } +}