From 03b42933fb4da0a4ed025a1b46fbbc3b91c30422 Mon Sep 17 00:00:00 2001 From: Vladimir Orany Date: Fri, 31 May 2019 12:25:34 +0200 Subject: [PATCH 1/4] removed the API Gateway integration in favor of official one --- docs/api-proxy.adoc | 295 +------ examples/gradle/lambda.gradle | 5 +- examples/planets/build.gradle | 2 +- .../examples/planets/MicronautHandler.java | 69 ++ .../examples/planets/PlanetController.groovy | 11 +- .../planets/PlanetNotFoundException.java | 34 + .../planets/PlanetControllerSpec.groovy | 19 +- examples/spacecrafts/build.gradle | 2 +- .../spacecrafts/MicronautHandler.java | 66 ++ .../spacecrafts/SpacecraftController.groovy | 20 +- .../SpacecraftNotFoundException.java | 34 + .../SpacecraftControllerSpec.groovy | 15 +- gradle.properties | 12 +- micronaut-function-aws-agp/build.gradle | 14 - .../micronaut/agp/ApiGatewayProxyHandler.java | 149 ---- .../agp/ApiGatewayProxyHttpRequest.java | 213 ----- .../src/main/resources/application.yml | 3 - .../src/main/resources/logback.xml | 14 - .../agp/ApiGatewayProxyHttpRequestSpec.groovy | 32 - micronaut-http-server-basic/build.gradle | 7 - .../http/basic/BasicRequestHandler.java | 811 ------------------ ...sicSystemFileCustomizableResponseType.java | 118 --- .../build.gradle | 7 - .../netty/tests/NettyHttpServerSpec.groovy | 28 - micronaut-http-server-tck/build.gradle | 5 - ...tractApiGatewayProxyHttpRequestSpec.groovy | 69 -- .../main/groovy/hello/galaxy/Greetings.groovy | 14 - .../hello/galaxy/HelloController.groovy | 43 - settings.gradle | 4 - 29 files changed, 260 insertions(+), 1855 deletions(-) create mode 100644 examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java create mode 100644 examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetNotFoundException.java create mode 100644 examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/MicronautHandler.java create mode 100644 examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftNotFoundException.java delete mode 100644 micronaut-function-aws-agp/build.gradle delete mode 100644 micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHandler.java delete mode 100644 micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequest.java delete mode 100644 micronaut-function-aws-agp/src/main/resources/application.yml delete mode 100644 micronaut-function-aws-agp/src/main/resources/logback.xml delete mode 100644 micronaut-function-aws-agp/src/test/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequestSpec.groovy delete mode 100644 micronaut-http-server-basic/build.gradle delete mode 100644 micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicRequestHandler.java delete mode 100644 micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicSystemFileCustomizableResponseType.java delete mode 100644 micronaut-http-server-tck-netty-tests/build.gradle delete mode 100644 micronaut-http-server-tck-netty-tests/src/test/groovy/com/agorapulse/micronaut/http/server/tck/netty/tests/NettyHttpServerSpec.groovy delete mode 100644 micronaut-http-server-tck/build.gradle delete mode 100644 micronaut-http-server-tck/src/main/groovy/com/agorapulse/micronaut/http/server/tck/AbstractApiGatewayProxyHttpRequestSpec.groovy delete mode 100644 micronaut-http-server-tck/src/main/groovy/hello/galaxy/Greetings.groovy delete mode 100644 micronaut-http-server-tck/src/main/groovy/hello/galaxy/HelloController.groovy diff --git a/docs/api-proxy.adoc b/docs/api-proxy.adoc index 3f6d4a4..2db859f 100644 --- a/docs/api-proxy.adoc +++ b/docs/api-proxy.adoc @@ -1,293 +1,17 @@ == Micronaut for API Gateway Proxy -API Gateway Lambda Proxy support for Micronaut which enables using most of the Micronaut HTTP Server features such -as controllers, filters and annotation statuses. Follow http://docs.micronaut.io/latest/guide/index.html[Micronaut website for extensive documentation]. - -You develop your application as you would develop any other server application using Micronaut HTTP capabilities. For example you -can create following controller - -[source,groovy,indent=0,options="nowrap"] -.Example Controller ----- -include::../examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy[] ----- - -This controller would be able to handle following URIs and methods after deployment using Micronaut for API Gateway Proxy: - - * `GET /planet/{star}` - * `GET /planet/{star}/{name}` - * `POST /planet/{star}/{name}` - * `DELETE /planet/{star}/{name}` - -WARNING: This library helps with translating API Gateway Proxy requests and responses into their Micronaut counterparts. It currently does not handle creating the -API mappings on the AWS. These needs to be created manually and must match the URL routes - - -A top of the standard features, you can use `api_gateway_proxy` https://docs.micronaut.io/latest/guide/index.html#environments[environment] -to distinguish the application is running using this library. - -Also following beans can be injected if necessary: - - * `com.amazonaws.services.lambda.runtime.Context` - * `com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent` - * `com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent.ProxyRequestContext` - * `com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent.RequestIdentity` - -WARNING: Application context is shared for the lifetime of the Lambda instance. Request related beans are reset before each execution. -This is a standard behaviour of Micronaut functions to take benefit from the hot deployments. - - -=== Installation - -Easiest way how to start is to fork https://github.com/agorapulse/micronaut-aws-api-gateway-proxy-starter[Micronaut AWS API Gateway Proxy Starter Project]. - -NOTE: For lack of Maven skils, this guide explains Gradle build setup only. - -If you want to add the library to existing project then you need to do a manual setup. - -==== Gradle `buildSrc` - -To prevent errors from machines which does not have any AWS credentials set you need to provides `LambdaHelper` class -to obtain the AWS account ID for the deployments. It is also a convenient place to manage all the dependencies for -the build scripts. - -[source,indent=0,options="nowrap"] -.buildSrc/build.gradle ----- -apply plugin: 'groovy' // <1> - -repositories { - jcenter() - mavenCentral() - maven { url 'https://plugins.gradle.org/m2/' } -} - -dependencies { - compile gradleApi() - compile localGroovy() - - compile 'jp.classmethod.aws:gradle-aws-plugin:0.38' // <2> - compile 'io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE' // <3> - compile "com.github.jengelman.gradle.plugins:shadow:4.0.2" // <4> - compile 'net.ltgt.gradle:gradle-apt-plugin:0.19' // <5> -} ----- -<1> Applying Groovy plugin for the `buildSrc` itself (for `LambdaHelper` implementation) -<2> Gradle AWS plugin used for deployment -<3> Gradle Dependency Management plugin used for managing Micronaut dependencies -<4> Gradle Shadow plugin optionally used by the local server subproject -<5> Gradle APT plugin used for Micronaut annotation processing - +API Gateway Lambda Proxy support for Micronaut has been replaced by an official suport +https://micronaut-projects.github.io/micronaut-aws/latest/guide/#apiProxy[Micronaut AWS API Gateway Support] [source,groovy,indent=0,options="nowrap"] -.buildSrc/src/main/groovy/lambda/LambdaHelper.groovy ----- -package lambda - -import com.amazonaws.SdkClientException -import groovy.transform.CompileStatic -import jp.classmethod.aws.gradle.AwsPluginExtension -import org.gradle.api.Project - -@CompileStatic -class LambdaHelper { - - private LambdaHelper() { } - - // see https://github.com/classmethod/gradle-aws-plugin/pull/160 - static String getAwsAccountId(Project project) { // <1> - try { - return project.getExtensions().getByType(AwsPluginExtension).accountId - } catch (SdkClientException ignored) { - project.logger.lifecycle("AWS credentials not configured!") - return '000000000000' - } - } - -} ----- -<1> Helper class provides single method to obtains AWS account id safely - - -==== Shared Gradle Files - -As far as you expect all the subprojects to be Micronaut projects you can share the configuration in the root `build.gradle` file. -Following configuration will enable subprojects with full Java and Groovy Micronaut support. - - -[source,indent=0,options="nowrap",subs='verbatim,attributes'] -.build.gradle ----- -subprojects { - - apply plugin: "groovy" - apply plugin: "io.spring.dependency-management" - apply plugin: "com.github.johnrengelman.shadow" - apply plugin: "net.ltgt.apt-eclipse" - apply plugin: "net.ltgt.apt-idea" - - - version "0.1" - group "micronaut.aws.api.gateway.proxy.starter" - - repositories { - mavenLocal() - mavenCentral() - maven { url "https://jcenter.bintray.com" } - } - - dependencyManagement { - imports { - mavenBom "io.micronaut:micronaut-bom:{version}" - } - } - - dependencies { - annotationProcessor "io.micronaut:micronaut-inject-java" - annotationProcessor "io.micronaut:micronaut-validation" - - compile "io.micronaut:micronaut-inject" - compile "io.micronaut:micronaut-validation" - compile "io.micronaut:micronaut-runtime" - compile "io.micronaut:micronaut-runtime-groovy" - - compileOnly "io.micronaut:micronaut-inject-java" - compileOnly "io.micronaut:micronaut-inject-groovy" - - runtime "ch.qos.logback:logback-classic:1.2.3" - - testCompile("org.spockframework:spock-core") { - exclude group: "org.codehaus.groovy", module: "groovy-all" - } - - testCompile "io.micronaut:micronaut-inject-groovy" - testCompile "io.micronaut:micronaut-inject-java" - - testCompile "junit:junit:4.12" - testCompile "org.hamcrest:hamcrest-all:1.3" - } - - compileJava.options.compilerArgs += '-parameters' - compileTestJava.options.compilerArgs += '-parameters' -} ----- - -Each AWS Lambda subproject also shares common setup. We can store it in `gradle/lambda.gradle` file. - -[source,indent=0,options="nowrap",subs='verbatim,attributes'] -.gradle/lambda.gradle ----- -configurations { - lambdaCompile.extendsFrom runtime // <1> - testCompile.extendsFrom lambdaCompile // <2> -} - -dependencies { - lambdaCompile "com.agorapulse:micronaut-function-aws-agp:{version}" // <3> - - compile "io.micronaut:micronaut-http-server" // <4> - compile "io.micronaut:micronaut-router" // <5> - - // gru for aws lambda can help you testing lambda fuctions - // https://agorapulse.github.io/gru/ - testCompile "com.agorapulse:gru-api-gateway:0.6.6" // <6> -} - -task buildZip(type: Zip) { // <7> - from compileJava - from compileGroovy - from processResources - into('lib') { - from configurations.lambdaCompile - } -} - -build.dependsOn buildZip // <8> ----- -<1> Create new configuration `lambdaCompile` to be used only for tests and deployed package -<2> Include `lambdaCompile` libraries in also in tests -<3> The integration library is only important for the package deployed -<4> Use just `micronaut-http-server` library as a dependency (not `micronaut-http-server-netty`) -<5> Micronaut router is also required -<6> You can optionally use https://agorapulse.github.io/gru/#_aws_api_gateway[Gru] for testing -<7> Adds a task to build Lambda deployment archive -<8> Adds `buildZip` to the default `build` task - - -==== API Gateway Subproject Build File - -Every API Gateway Lambda project must at least contain following definition of the deployment -as well as it needs to apply the shared `lambda.gradle` file. - -[source,indent=0,options="nowrap"] -.build.gradle +.Example `MicronautHandler` ---- -import lambda.LambdaHelper -import com.amazonaws.services.lambda.model.Runtime -import jp.classmethod.aws.gradle.lambda.AWSLambdaMigrateFunctionTask - -apply from: '../gradle/lambda.gradle' // <1> - -task deployLambda( // <2> - type: AWSLambdaMigrateFunctionTask, - dependsOn: build, - group: 'deploy' -) { - functionName = 'MicronautHelloWorld' - handler = 'com.agorapulse.micronaut.agp.ApiGatewayProxyHandler::handleRequest' // <3> - role = "arn:aws:iam::${LambdaHelper.getAwsAccountId(project)}:role/lambda_basic_execution" - runtime = Runtime.Java8 // <4> - zipFile = buildZip.archivePath // <5> - memorySize = 1024 - timeout = 30 -} ----- -<1> Import helper Gradle script -<2> Add task to deploy to AWS Lambda -<3> Lambda function handler must be `com.agorapulse.micronaut.agp.ApiGatewayProxyHandler::handleRequest` -<4> Runtime must be `Java8` -<5> Archive must be the result of `buildZip task - -==== Local Server - -The biggest advantage of Micronaut for Api Gateway Proxy integration library is the ability to easily run locally. - - -[source,indent=0,options="nowrap"] -.build.gradle +include::../examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java[] ---- -apply plugin: 'application' // <1> - -dependencies { - compile project(':hello-world') // <2> - - compile "io.micronaut:micronaut-http-server-netty" // <3> - - testCompile "com.agorapulse:gru-http:0.6.6" // <4> -} - -shadowJar { // <5> - mergeServiceFiles() -} - -run.jvmArgs('-noverify', '-XX:TieredStopAtLevel=1') - -mainClassName = "starter.Application" // <6> ----- -<1> Apply `application` plugin so you will be able to run the server as application locally -<2> Depend on every API Gateway subproject you want to include into the local server -<3> You need real Micronaut's HTTP server implementation to run the server -<4> You can optionally use https://agorapulse.github.io/gru/#_http[Gru] for testing the local server -<5> If you decide to run from Shadow JAR you need to merge the service files -<6> Replace with your own application class - === Testing -The easiest way to test the API Gateway Proxy integration is using https://agorapulse.github.io/gru/#_aws_api_gateway[Gru for API Gateway] -testing client. The library should be already on the classpath if you have followed the steps or if -you are using the starter project. - +You can still use the API Gateway Proxy integration is using https://agorapulse.github.io/gru/#_aws_api_gateway[Gru for API Gateway] [source,groovy,indent=0,options="nowrap"] .Controller Spec @@ -295,11 +19,10 @@ you are using the starter project. include::../examples/planets/src/test/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetControllerSpec.groovy[] ---- <1> Use `ApiGatewayProxy` client with https://agorapulse.github.io/gru[Gru] -<2> Configure which URLs and methods are handled by the Micronaut, the handler must always be `ApiGatewayProxyHandler` -or its successors -<3> You can customize the handler initialization by providing a mock beans -<4> Test method using https://agorapulse.github.io/gru[Gru] +<2> Delegate to `MicronautHandler` (see above) +<3> Reset the application context +<4> Make changes in the application context +<5> Test method using https://agorapulse.github.io/gru[Gru] TIP: The advantage of using Gru is that you can reuse the existing test with the local server if required. Only thing which changes it the handler setup and the client being used (HTTP instead of API Gateway Proxy). - diff --git a/examples/gradle/lambda.gradle b/examples/gradle/lambda.gradle index 7e7b351..6e948de 100644 --- a/examples/gradle/lambda.gradle +++ b/examples/gradle/lambda.gradle @@ -4,10 +4,7 @@ configurations { } dependencies { - lambda project(':micronaut-function-aws-agp') - - compile "io.micronaut:micronaut-http-server" - compile "io.micronaut:micronaut-router" + compile 'io.micronaut.aws:micronaut-function-aws-api-proxy' compile project(':micronaut-aws-sdk') compile "com.amazonaws:aws-java-sdk-dynamodb:$awsSdkVersion" diff --git a/examples/planets/build.gradle b/examples/planets/build.gradle index 1eb9467..570ae3a 100644 --- a/examples/planets/build.gradle +++ b/examples/planets/build.gradle @@ -23,7 +23,7 @@ dependencies { task deployLambda(type: AWSLambdaMigrateFunctionTask, dependsOn: build, group: 'deploy') { functionName = 'MicronautExamplePlanets' - handler = 'com.agorapulse.micronaut.agp.ApiGatewayProxyHandler::handleRequest' + handler = 'com.agorapulse.micronaut.http.examples.planets.MicronautHandler::handleRequest' role = "arn:aws:iam::281741939716:role/service-role/MicronautExamples" runtime = Runtime.Java8 zipFile = buildZip.archivePath diff --git a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java new file mode 100644 index 0000000..11ecb7f --- /dev/null +++ b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java @@ -0,0 +1,69 @@ +package com.agorapulse.micronaut.http.examples.planets; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.ApplicationContextBuilder; +import io.micronaut.function.aws.proxy.MicronautLambdaContainerHandler; +import io.micronaut.http.HttpRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.function.Consumer; + +import static com.amazonaws.serverless.proxy.RequestReader.LAMBDA_CONTEXT_PROPERTY; + +public class MicronautHandler implements RequestStreamHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(MicronautHandler.class); + + private static MicronautLambdaContainerHandler handler; + private static ApplicationContextBuilder builder; + + static { + reset(); + } + + /** + * Resets the current handler. For testing purposes only. + */ + public static void reset() { + reset(b -> {}); + } + + /** + * Resets the current handler. For testing purposes only. + * + * @param configuration builder customizer + */ + public static void reset(Consumer configuration) { + try { + builder = ApplicationContext.build(); + configuration.accept(builder); + handler = MicronautLambdaContainerHandler.getAwsProxyHandler(builder); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Exception in container initialization", e); + } + throw new IllegalStateException("Could not initialize Micronaut", e); + } + } + + public static ApplicationContext getApplicationContext() { + if (handler == null) { + reset(); + } + return handler.getApplicationContext(); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} diff --git a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy index 5faf910..36ef367 100644 --- a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy +++ b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy @@ -3,6 +3,7 @@ package com.agorapulse.micronaut.http.examples.planets import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Delete +import io.micronaut.http.annotation.Error import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post import io.micronaut.http.annotation.Status @@ -26,7 +27,11 @@ class PlanetController { @Get('/{star}/{name}') Planet show(String star, String name) { - return planetDBService.get(star, name) + Planet planet = planetDBService.get(star, name) + if (!planet) { + throw new PlanetNotFoundException(name) + } + return planet } @Post('/{star}/{name}') @Status(HttpStatus.CREATED) @@ -43,4 +48,8 @@ class PlanetController { return planet } + @Status(HttpStatus.NOT_FOUND) + @Error(PlanetNotFoundException) + void planetNotFound(PlanetNotFoundException ex) { } + } diff --git a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetNotFoundException.java b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetNotFoundException.java new file mode 100644 index 0000000..86712c7 --- /dev/null +++ b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetNotFoundException.java @@ -0,0 +1,34 @@ +package com.agorapulse.micronaut.http.examples.planets; + +public class PlanetNotFoundException extends RuntimeException { + + private final String planet; + + public PlanetNotFoundException(String planet) { + this.planet = planet; + } + + public PlanetNotFoundException(String planet, String message) { + super(message); + this.planet = planet; + } + + public PlanetNotFoundException(String planet, String message, Throwable cause) { + super(message, cause); + this.planet = planet; + } + + public PlanetNotFoundException(String planet, Throwable cause) { + super(cause); + this.planet = planet; + } + + public PlanetNotFoundException(String planet, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + this.planet = planet; + } + + public String getPlanet() { + return planet; + } +} diff --git a/examples/planets/src/test/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetControllerSpec.groovy b/examples/planets/src/test/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetControllerSpec.groovy index fe898dd..c6e4ca7 100644 --- a/examples/planets/src/test/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetControllerSpec.groovy +++ b/examples/planets/src/test/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetControllerSpec.groovy @@ -4,7 +4,6 @@ import com.agorapulse.dru.Dru import com.agorapulse.dru.dynamodb.persistence.DynamoDB import com.agorapulse.gru.Gru import com.agorapulse.gru.agp.ApiGatewayProxy -import com.agorapulse.micronaut.agp.ApiGatewayProxyHandler import com.amazonaws.services.dynamodbv2.datamodeling.IDynamoDBMapper import io.micronaut.context.ApplicationContext import org.junit.Rule @@ -16,28 +15,24 @@ import spock.lang.Specification class PlanetControllerSpec extends Specification { @Rule private final Gru gru = Gru.equip(ApiGatewayProxy.steal(this) { // <1> - map '/planet/{star}' to ApiGatewayProxyHandler // <2> - map '/planet/{star}/{name}' to ApiGatewayProxyHandler + map '/planet/{star}' to MicronautHandler // <2> + map '/planet/{star}/{name}' to MicronautHandler }) @Rule private final Dru dru = Dru.steal(this) - @SuppressWarnings('UnusedPrivateField') - private final ApiGatewayProxyHandler handler = new ApiGatewayProxyHandler() { - @Override - protected void doWithApplicationContext(ApplicationContext ctx) { // <3> - ctx.registerSingleton(IDynamoDBMapper, DynamoDB.createMapper(dru)) - } - } - void setup() { + MicronautHandler.reset() // <3> + MicronautHandler.applicationContext.with { ApplicationContext ctx -> + ctx.registerSingleton(IDynamoDBMapper, DynamoDB.createMapper(dru)) // <4> + } dru.add(new Planet(star: 'sun', name: 'mercury')) dru.add(new Planet(star: 'sun', name: 'venus')) dru.add(new Planet(star: 'sun', name: 'earth')) dru.add(new Planet(star: 'sun', name: 'mars')) } - void 'get planet'() { // <4> + void 'get planet'() { // <5> expect: gru.test { get('/planet/sun/earth') diff --git a/examples/spacecrafts/build.gradle b/examples/spacecrafts/build.gradle index 36f725a..0817ac1 100644 --- a/examples/spacecrafts/build.gradle +++ b/examples/spacecrafts/build.gradle @@ -23,7 +23,7 @@ dependencies { task deployLambda(type: AWSLambdaMigrateFunctionTask, dependsOn: build, group: 'deploy') { functionName = 'MicronautExampleSpacecrafts' - handler = 'com.agorapulse.micronaut.agp.ApiGatewayProxyHandler::handleRequest' + handler = 'com.agorapulse.micronaut.http.examples.spacecrafts.MicronautHandler::handleRequest' role = "arn:aws:iam::281741939716:role/service-role/MicronautExamples" runtime = Runtime.Java8 zipFile = buildZip.archivePath diff --git a/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/MicronautHandler.java b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/MicronautHandler.java new file mode 100644 index 0000000..06960c5 --- /dev/null +++ b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/MicronautHandler.java @@ -0,0 +1,66 @@ +package com.agorapulse.micronaut.http.examples.spacecrafts; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestStreamHandler; +import io.micronaut.context.ApplicationContext; +import io.micronaut.context.ApplicationContextBuilder; +import io.micronaut.function.aws.proxy.MicronautLambdaContainerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.function.Consumer; + +public class MicronautHandler implements RequestStreamHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(MicronautHandler.class); + + private static MicronautLambdaContainerHandler handler; + private static ApplicationContextBuilder builder; + + static { + reset(); + } + + /** + * Resets the current handler. For testing purposes only. + */ + public static void reset() { + reset(b -> {}); + } + + /** + * Resets the current handler. For testing purposes only. + * + * @param configuration builder customizer + */ + public static void reset(Consumer configuration) { + try { + builder = ApplicationContext.build(); + configuration.accept(builder); + handler = MicronautLambdaContainerHandler.getAwsProxyHandler(builder); + } catch (ContainerInitializationException e) { + // if we fail here. We re-throw the exception to force another cold start + if (LOGGER.isErrorEnabled()) { + LOGGER.error("Exception in container initialization", e); + } + throw new IllegalStateException("Could not initialize Micronaut", e); + } + } + + public static ApplicationContext getApplicationContext() { + if (handler == null) { + reset(); + } + return handler.getApplicationContext(); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) + throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} diff --git a/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy index f714c65..b0880f4 100644 --- a/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy +++ b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy @@ -3,6 +3,7 @@ package com.agorapulse.micronaut.http.examples.spacecrafts import io.micronaut.http.HttpStatus import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Delete +import io.micronaut.http.annotation.Error import io.micronaut.http.annotation.Get import io.micronaut.http.annotation.Post import io.micronaut.http.annotation.Status @@ -26,21 +27,34 @@ class SpacecraftController { @Get('/{country}/{name}') Spacecraft show(String country, String name) { - return spacecraftDBService.get(country, name) + Spacecraft spacecraft = spacecraftDBService.get(country, name) + + if (!spacecraft) { + throw new SpacecraftNotFoundException(name) + } + + return spacecraft } - @Post('/{country}/{name}') @Status(HttpStatus.CREATED) + @Post('/{country}/{name}') + @Status(HttpStatus.CREATED) Spacecraft save(String country, String name) { Spacecraft planet = new Spacecraft(country: country, name: name) spacecraftDBService.save(planet) return planet } - @Delete('/{country}/{name}') @Status(HttpStatus.NO_CONTENT) + @Delete('/{country}/{name}') + @Status(HttpStatus.NO_CONTENT) Spacecraft delete(String country, String name) { Spacecraft planet = show(country, name) spacecraftDBService.delete(planet) return planet } + + @Status(HttpStatus.NOT_FOUND) + @Error(SpacecraftNotFoundException) + void spacecraftNotFound(SpacecraftNotFoundException ex) {} + } diff --git a/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftNotFoundException.java b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftNotFoundException.java new file mode 100644 index 0000000..e163b56 --- /dev/null +++ b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftNotFoundException.java @@ -0,0 +1,34 @@ +package com.agorapulse.micronaut.http.examples.spacecrafts; + +public class SpacecraftNotFoundException extends RuntimeException { + + private final String spacecraft; + + public SpacecraftNotFoundException(String spacecraft) { + this.spacecraft = spacecraft; + } + + public SpacecraftNotFoundException(String spacecraft, String message) { + super(message); + this.spacecraft = spacecraft; + } + + public SpacecraftNotFoundException(String spacecraft, String message, Throwable cause) { + super(message, cause); + this.spacecraft = spacecraft; + } + + public SpacecraftNotFoundException(String spacecraft, Throwable cause) { + super(cause); + this.spacecraft = spacecraft; + } + + public SpacecraftNotFoundException(String spacecraft, String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + this.spacecraft = spacecraft; + } + + public String getSpacecraft() { + return spacecraft; + } +} diff --git a/examples/spacecrafts/src/test/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftControllerSpec.groovy b/examples/spacecrafts/src/test/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftControllerSpec.groovy index 8c64873..162e0ff 100644 --- a/examples/spacecrafts/src/test/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftControllerSpec.groovy +++ b/examples/spacecrafts/src/test/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftControllerSpec.groovy @@ -4,7 +4,6 @@ import com.agorapulse.dru.Dru import com.agorapulse.dru.dynamodb.persistence.DynamoDB import com.agorapulse.gru.Gru import com.agorapulse.gru.agp.ApiGatewayProxy -import com.agorapulse.micronaut.agp.ApiGatewayProxyHandler import com.amazonaws.services.dynamodbv2.datamodeling.IDynamoDBMapper import io.micronaut.context.ApplicationContext import org.junit.Rule @@ -16,21 +15,17 @@ import spock.lang.Specification class SpacecraftControllerSpec extends Specification { @Rule private final Gru gru = Gru.equip(ApiGatewayProxy.steal(this) { - map '/spacecraft/{country}' to ApiGatewayProxyHandler - map '/spacecraft/{country}/{name}' to ApiGatewayProxyHandler + map '/spacecraft/{country}' to MicronautHandler + map '/spacecraft/{country}/{name}' to MicronautHandler }) @Rule private final Dru dru = Dru.steal(this) - @SuppressWarnings('UnusedPrivateField') - private final ApiGatewayProxyHandler handler = new ApiGatewayProxyHandler() { - @Override - protected void doWithApplicationContext(ApplicationContext ctx) { + void setup() { + MicronautHandler.reset() + MicronautHandler.applicationContext.with { ApplicationContext ctx -> ctx.registerSingleton(IDynamoDBMapper, DynamoDB.createMapper(dru)) } - } - - void setup() { dru.add(new Spacecraft(country: 'russia', name: 'vostok')) dru.add(new Spacecraft(country: 'usa', name: 'dragon')) } diff --git a/gradle.properties b/gradle.properties index 4a6fa59..a62613a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,11 @@ -version = 1.1.0.1 -micronautVersion = 1.1.0 -gruVersion = 0.8.0 +version = 1.1.2 +micronautVersion = 1.1.2 +gruVersion = 0.8.1 druVersion = 0.6.0 -groovyVersion = 2.5.6 +groovyVersion = 2.5.7 spockVersion = 1.3-groovy-2.5 -awsSdkVersion = 1.11.542 -testcontainersVersion = 1.11.2 +awsSdkVersion = 1.11.256 +testcontainersVersion = 1.11.3 # this should be aligned to Micronaut version # required for AWS CBOR marshalling diff --git a/micronaut-function-aws-agp/build.gradle b/micronaut-function-aws-agp/build.gradle deleted file mode 100644 index 70d0f64..0000000 --- a/micronaut-function-aws-agp/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -dependencies { - compile project(':micronaut-http-server-basic') - - // custom compile - compile group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.2.0' - compile group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '2.2.5' - - // custom test - testCompile project(':micronaut-http-server-tck') - - testCompile "com.agorapulse:gru-api-gateway:$gruVersion" -} - -apply from: '../gradle/bintray.gradle' diff --git a/micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHandler.java b/micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHandler.java deleted file mode 100644 index 63bc32b..0000000 --- a/micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHandler.java +++ /dev/null @@ -1,149 +0,0 @@ -package com.agorapulse.micronaut.agp; - -import com.agorapulse.micronaut.http.basic.BasicRequestHandler; -import com.amazonaws.services.lambda.runtime.*; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.micronaut.context.ApplicationContext; -import io.micronaut.context.env.Environment; -import io.micronaut.context.env.PropertySource; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.http.HttpRequest; -import io.micronaut.http.HttpResponse; -import io.netty.buffer.ByteBuf; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -public class ApiGatewayProxyHandler implements RequestHandler { - - private static final Logger LOG = LoggerFactory.getLogger(BasicRequestHandler.class); - - /** - * This is the name of the environment which is set if the application in executed by this handler. - */ - public static final String API_GATEWAY_PROXY_ENVIRONMENT = "api_gateway_proxy"; - - private final ApplicationContext context; - - public ApiGatewayProxyHandler() { - if (LOG.isDebugEnabled()) { - LOG.debug("Starting application context initialization"); - } - this.context = buildApplicationContext(); - startEnvironment(context); - if (LOG.isDebugEnabled()) { - LOG.debug("Application context initialization finished"); - } - } - - // tag::handler[] - @Override - public final APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context lambdaContext) { - if (LOG.isDebugEnabled()) { - LOG.debug("Starting registration of custom request beans"); - } - registerContextBeans(lambdaContext); - - context.registerSingleton(input); - - APIGatewayProxyRequestEvent.ProxyRequestContext requestContext = Optional.ofNullable(input.getRequestContext()).orElseGet(APIGatewayProxyRequestEvent.ProxyRequestContext::new); - APIGatewayProxyRequestEvent.RequestIdentity requestIdentity = Optional.ofNullable(requestContext.getIdentity()).orElseGet(APIGatewayProxyRequestEvent.RequestIdentity::new); - - context.registerSingleton(requestContext); - context.registerSingleton(requestIdentity); - - doWithApplicationContext(context); - - if (LOG.isDebugEnabled()) { - LOG.debug("Registration of custom request beans finished"); - } - - ObjectMapper mapper = context.getBean(ObjectMapper.class); - ConversionService conversionService = context.getConversionService(); - HttpRequest request = convertToMicronautHttpRequest(input, conversionService, mapper); // <1> - BasicRequestHandler inBoundHandler = context.getBean(BasicRequestHandler.class); // <2> - HttpResponse response = inBoundHandler.handleRequest(request); // <3> - return convertToApiGatewayProxyResponse(response); // <4> - } - // end::handler[] - - protected void doWithApplicationContext(ApplicationContext applicationContext) { - // an entry point to customize application context, useful for tests - } - - protected HttpRequest convertToMicronautHttpRequest(APIGatewayProxyRequestEvent input, ConversionService conversionService, ObjectMapper mapper) { - return ApiGatewayProxyHttpRequest.create(input, conversionService, mapper); - } - - protected APIGatewayProxyResponseEvent convertToApiGatewayProxyResponse(HttpResponse response) { - APIGatewayProxyResponseEvent responseEvent = new APIGatewayProxyResponseEvent().withStatusCode(response.status().getCode()); - - if (response.body() instanceof ByteBuf) { - ByteBuf buffer = (ByteBuf) response.body(); - byte[] bytes = new byte[buffer.readableBytes()]; - buffer.readBytes(bytes); - responseEvent.setBody(new String(bytes)); - } else if (response.body() != null) { - throw new IllegalStateException("Body is not ByteBuf: " + response.body()); - } - - responseEvent.setHeaders(response - .getHeaders() - .asMap() - .entrySet() - .stream() - .collect(Collectors.toMap(Map.Entry::getKey, entry -> entry.getValue().get(0))) - ); - - return responseEvent; - } - - protected ApplicationContext buildApplicationContext() { - return ApplicationContext.build().environments( - API_GATEWAY_PROXY_ENVIRONMENT, - Environment.AMAZON_EC2, - Environment.CLOUD - ).build(); - } - - /** - * Register the beans in the application. - * - * @param lambdaContext context - */ - protected void registerContextBeans(Context lambdaContext) { - context.registerSingleton(lambdaContext); - LambdaLogger logger = lambdaContext.getLogger(); - if (logger != null) { - context.registerSingleton(logger); - } - ClientContext clientContext = lambdaContext.getClientContext(); - if (clientContext != null) { - context.registerSingleton(clientContext); - } - CognitoIdentity identity = lambdaContext.getIdentity(); - if (identity != null) { - context.registerSingleton(identity); - } - } - - /** - * Start the environment specified. - * @param applicationContext the application context with the environment - * @return The environment within the context - */ - protected Environment startEnvironment(ApplicationContext applicationContext) { - if (this instanceof PropertySource) { - applicationContext.getEnvironment().addPropertySource((PropertySource) this); - } - - return applicationContext - .start() - .getEnvironment(); - } -} diff --git a/micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequest.java b/micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequest.java deleted file mode 100644 index 178110c..0000000 --- a/micronaut-function-aws-agp/src/main/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequest.java +++ /dev/null @@ -1,213 +0,0 @@ -package com.agorapulse.micronaut.agp; - -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.micronaut.core.convert.ArgumentConversionContext; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.convert.value.MutableConvertibleValues; -import io.micronaut.core.convert.value.MutableConvertibleValuesMap; -import io.micronaut.http.*; -import io.micronaut.http.cookie.Cookie; -import io.micronaut.http.cookie.Cookies; -import io.micronaut.http.simple.SimpleHttpParameters; - -import java.io.IOException; -import java.net.InetSocketAddress; -import java.net.URI; -import java.util.*; -import java.util.regex.Pattern; - -abstract class ApiGatewayProxyHttpRequest implements HttpRequest { - - private static final String LOCAL_DEVELOPMENT_APP_ID = "local"; - private static final String LOCAL_DEVELOPMENT_SOURCE_IP = "127.0.0.1"; - private static final String LOCAL_DEVELOPMENT_AWS_REGION = "eu-west-1"; - - static HttpRequest create(APIGatewayProxyRequestEvent input, ConversionService conversionService, ObjectMapper mapper) { - if (input.getIsBase64Encoded() == Boolean.TRUE) { - return new BinaryApiGatewayProxyHttpRequest(input, conversionService); - } - - boolean isText = Optional - .ofNullable(input.getHeaders()) - .flatMap(headers -> Optional.ofNullable(headers.get("Content-Type"))) - .map(contentType -> contentType.contains("text")) - .orElse(false); - - if (isText) { - return new TextApiGatewayProxyHttpRequest(input, conversionService); - } - - return new JsonApiGatewayProxyHttpRequest(input, conversionService, mapper); - } - - private static final int HTTPS_PORT = 443; - private static final String AWS_REGION_ENV_NAME = "AWS_REGION"; - private static final String DEFAULT_HTTP_METHOD = "GET"; - - private static class BinaryApiGatewayProxyHttpRequest extends ApiGatewayProxyHttpRequest { - BinaryApiGatewayProxyHttpRequest(APIGatewayProxyRequestEvent input, ConversionService conversionService) { - super(input, conversionService); - } - - @Override - public Optional getBody() { - return Optional.ofNullable(input.getBody()).map(encoded -> Base64.getDecoder().decode(encoded)); - } - } - - private static class TextApiGatewayProxyHttpRequest extends ApiGatewayProxyHttpRequest { - TextApiGatewayProxyHttpRequest(APIGatewayProxyRequestEvent input, ConversionService conversionService) { - super(input, conversionService); - } - - @Override - public Optional getBody() { - return Optional.ofNullable(input.getBody()); - } - } - - /** - * I don't know how any other way how to tell Micronaut that the body is JSON so I parse it for it - */ - private static class JsonApiGatewayProxyHttpRequest extends ApiGatewayProxyHttpRequest { - - private final ObjectMapper mapper; - - JsonApiGatewayProxyHttpRequest(APIGatewayProxyRequestEvent input, ConversionService conversionService, ObjectMapper mapper) { - super(input, conversionService); - this.mapper = mapper; - } - - @Override - public Optional getBody() { - return Optional.ofNullable(input.getBody()).map(body -> { - try { - return mapper.reader().readTree(body); - } catch (IOException e) { - throw new IllegalStateException("Unparsable body: " + input.getBody(), e); - } - }); - } - } - - private static class EmptyCookies implements Cookies { - static final Cookies INSTANCE = new EmptyCookies(); - - @Override - public Set getAll() { - return Collections.emptySet(); - } - - @Override - public Optional findCookie(CharSequence name) { - return Optional.empty(); - } - - @Override - public Optional get(CharSequence name, ArgumentConversionContext conversionContext) { - return Optional.empty(); - } - - @Override - public Collection values() { - return Collections.emptySet(); - } - } - - final APIGatewayProxyRequestEvent input; - - private final SimpleHttpParameters parameters; - private final HttpMethod method; - private final URI uri; - private final MutableConvertibleValues attributes = new MutableConvertibleValuesMap<>(); - private final MutableHttpHeaders headers; - private final InetSocketAddress remoteAddress; - - ApiGatewayProxyHttpRequest(APIGatewayProxyRequestEvent input, ConversionService conversionService) { - this.input = input; - - String httpMethod = Optional.ofNullable(input.getHttpMethod()).orElse(DEFAULT_HTTP_METHOD); - this.method = HttpMethod.valueOf(httpMethod.toUpperCase()); - - APIGatewayProxyRequestEvent.ProxyRequestContext context = Optional.ofNullable(input.getRequestContext()).orElseGet(() -> - new APIGatewayProxyRequestEvent.ProxyRequestContext() - .withApiId(LOCAL_DEVELOPMENT_APP_ID) - ); - - String region = Optional.ofNullable(System.getenv(AWS_REGION_ENV_NAME)).orElseGet(() -> LOCAL_DEVELOPMENT_AWS_REGION); - String serverName = context.getApiId() + ".execute-api." + region + ".amazonaws.com"; - - APIGatewayProxyRequestEvent.RequestIdentity identity = Optional.ofNullable(context.getIdentity()).orElseGet(() -> - new APIGatewayProxyRequestEvent.RequestIdentity().withSourceIp(LOCAL_DEVELOPMENT_SOURCE_IP) - ); - - remoteAddress = new InetSocketAddress(identity.getSourceIp(), HTTPS_PORT); - - MutableHttpRequest request = HttpRequest.create(this.method, - "https://" + serverName + reconstructPath(Optional.ofNullable(input.getResource()).orElse(input.getPath()), input.getPathParameters()) - ); - - this.uri = request.getUri(); - - Map httpHeaders = Optional.ofNullable(input.getHeaders()).orElseGet(Collections::emptyMap); - this.headers = request.getHeaders(); - httpHeaders.forEach(this.headers::add); - - Map queryStringParameters = Optional.ofNullable(input.getQueryStringParameters()).orElseGet(Collections::emptyMap); - this.parameters = new SimpleHttpParameters(conversionService); - queryStringParameters.forEach(this.parameters::add); - } - - @Override - public Cookies getCookies() { - return EmptyCookies.INSTANCE; - } - - @Override - public HttpParameters getParameters() { - return parameters; - } - - @Override - public HttpMethod getMethod() { - return method; - } - - @Override - public URI getUri() { - return uri; - } - - @Override - public HttpHeaders getHeaders() { - return headers; - } - - @Override - public InetSocketAddress getRemoteAddress() { - return remoteAddress; - } - - @Override - public MutableConvertibleValues getAttributes() { - return attributes; - } - - @Override - public String toString() { - return getClass().getSimpleName() + ": " + input.toString(); - } - - static String reconstructPath(String resource, Map pathVariables) { - if (pathVariables == null) { - return resource; - } - String path = resource; - for (Map.Entry variable : pathVariables.entrySet()) { - path = path.replaceAll("\\{" + Pattern.quote(variable.getKey()) +"\\+?}", variable.getValue()); - } - return path; - } -} diff --git a/micronaut-function-aws-agp/src/main/resources/application.yml b/micronaut-function-aws-agp/src/main/resources/application.yml deleted file mode 100644 index 35f7b2b..0000000 --- a/micronaut-function-aws-agp/src/main/resources/application.yml +++ /dev/null @@ -1,3 +0,0 @@ -micronaut: - application: - name: hello-galaxy \ No newline at end of file diff --git a/micronaut-function-aws-agp/src/main/resources/logback.xml b/micronaut-function-aws-agp/src/main/resources/logback.xml deleted file mode 100644 index afaebf8..0000000 --- a/micronaut-function-aws-agp/src/main/resources/logback.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/micronaut-function-aws-agp/src/test/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequestSpec.groovy b/micronaut-function-aws-agp/src/test/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequestSpec.groovy deleted file mode 100644 index 854fb99..0000000 --- a/micronaut-function-aws-agp/src/test/groovy/com/agorapulse/micronaut/agp/ApiGatewayProxyHttpRequestSpec.groovy +++ /dev/null @@ -1,32 +0,0 @@ -package com.agorapulse.micronaut.agp - -import com.agorapulse.gru.Gru -import com.agorapulse.gru.agp.ApiGatewayProxy -import com.agorapulse.micronaut.http.server.tck.AbstractApiGatewayProxyHttpRequestSpec -import org.junit.Rule -import spock.lang.Unroll - -/** - * Tests for api gateway proxy HTTP request. - */ -class ApiGatewayProxyHttpRequestSpec extends AbstractApiGatewayProxyHttpRequestSpec { - - @Rule Gru gru = Gru.equip(ApiGatewayProxy.steal(this) { - map '/hello' to ApiGatewayProxyHandler - map '/hello/greet' to ApiGatewayProxyHandler - map '/hello/greet/{message}/{language}' to ApiGatewayProxyHandler - map '/hello/mfa' to ApiGatewayProxyHandler - }) - - @Unroll - void 'reconstruct path #path with variable #variables to #original'() { - expect: - ApiGatewayProxyHttpRequest.reconstructPath(resource, variables) == original - where: - original | resource | variables - '/foo/bar' | '/foo/bar' | null - '/foo/bar' | '/foo/{place}' | [place: 'bar'] - '/foo/bar' | '/{proxy+}' | [proxy: 'foo/bar'] - } - -} diff --git a/micronaut-http-server-basic/build.gradle b/micronaut-http-server-basic/build.gradle deleted file mode 100644 index 8b7abe1..0000000 --- a/micronaut-http-server-basic/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -dependencies { - compile "io.micronaut:micronaut-buffer-netty" - compile "io.micronaut:micronaut-http-server" - compile "io.micronaut:micronaut-router" -} - -apply from: '../gradle/bintray.gradle' diff --git a/micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicRequestHandler.java b/micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicRequestHandler.java deleted file mode 100644 index 6b6b238..0000000 --- a/micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicRequestHandler.java +++ /dev/null @@ -1,811 +0,0 @@ -package com.agorapulse.micronaut.http.basic; - -import io.micronaut.buffer.netty.NettyByteBufferFactory; -import io.micronaut.context.BeanLocator; -import io.micronaut.core.async.publisher.Publishers; -import io.micronaut.core.convert.ConversionService; -import io.micronaut.core.io.buffer.ByteBuffer; -import io.micronaut.core.type.ReturnType; -import io.micronaut.core.util.StreamUtils; -import io.micronaut.http.*; -import io.micronaut.http.annotation.Produces; -import io.micronaut.http.annotation.Status; -import io.micronaut.http.codec.MediaTypeCodec; -import io.micronaut.http.codec.MediaTypeCodecRegistry; -import io.micronaut.http.filter.HttpFilter; -import io.micronaut.http.filter.HttpServerFilter; -import io.micronaut.http.filter.ServerFilterChain; -import io.micronaut.http.hateoas.JsonError; -import io.micronaut.http.hateoas.Link; -import io.micronaut.http.server.binding.RequestArgumentSatisfier; -import io.micronaut.http.server.exceptions.ExceptionHandler; -import io.micronaut.http.server.exceptions.InternalServerException; -import io.micronaut.http.server.types.files.FileCustomizableResponseType; -import io.micronaut.inject.MethodExecutionHandle; -import io.micronaut.inject.qualifiers.Qualifiers; -import io.micronaut.runtime.http.codec.TextPlainCodec; -import io.micronaut.scheduling.executor.ExecutorSelector; -import io.micronaut.web.router.*; -import io.micronaut.web.router.exceptions.DuplicateRouteException; -import io.micronaut.web.router.exceptions.UnsatisfiedRouteException; -import io.micronaut.web.router.resource.StaticResourceResolver; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.reactivex.BackpressureStrategy; -import io.reactivex.Flowable; -import io.reactivex.schedulers.Schedulers; -import org.reactivestreams.Publisher; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.inject.Singleton; -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.nio.charset.Charset; -import java.nio.file.Paths; -import java.util.*; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; - -/** - * Simplified version of io.micronaut.http.server.netty.RoutingInBoundHandler. - * - * @since 1.0 - */ -@Singleton -public class BasicRequestHandler { - - private static final Logger LOG = LoggerFactory.getLogger(BasicRequestHandler.class); - - private final Router router; - private final ExecutorSelector executorSelector; - private final StaticResourceResolver staticResourceResolver; - private final ExecutorService ioExecutor; - private final BeanLocator beanLocator; - private final RequestArgumentSatisfier requestArgumentSatisfier; - private final MediaTypeCodecRegistry mediaTypeCodecRegistry; - - /** - * @param beanLocator The bean locator - * @param router The router - * @param mediaTypeCodecRegistry The media type codec registry - * @param staticResourceResolver The static resource resolver - * @param requestArgumentSatisfier The request argument satisfier - * @param executorSelector The executor selector - * @param ioExecutor The IO executor - */ - public BasicRequestHandler( - BeanLocator beanLocator, - Router router, - MediaTypeCodecRegistry mediaTypeCodecRegistry, - StaticResourceResolver staticResourceResolver, - RequestArgumentSatisfier requestArgumentSatisfier, - ExecutorSelector executorSelector, - ExecutorService ioExecutor - ) { - - this.mediaTypeCodecRegistry = mediaTypeCodecRegistry; - this.beanLocator = beanLocator; - this.staticResourceResolver = staticResourceResolver; - this.ioExecutor = ioExecutor; - this.executorSelector = executorSelector; - this.router = router; - this.requestArgumentSatisfier = requestArgumentSatisfier; - } - - @SuppressWarnings("unchecked") - private Flowable exceptionCaught(HttpRequest httpRequest, RouteMatch originalRoute, Throwable cause) { - RouteMatch errorRoute = null; - if (httpRequest == null) { - if (LOG.isErrorEnabled()) { - LOG.error("Micronaut Server Error - No request state present. Cause: " + cause.getMessage(), cause); - } - return Flowable.just(HttpResponse.serverError()); - } - - // find the origination of of the route - Class declaringType = null; - if (originalRoute instanceof MethodExecutionHandle) { - declaringType = ((MethodExecutionHandle) originalRoute).getDeclaringType(); - } - - // when arguments do not match, then there is UnsatisfiedRouteException, we can handle this with a routed bad request - if (cause instanceof UnsatisfiedRouteException) { - if (declaringType != null) { - // handle error with a method that is non global with bad request - errorRoute = router.route(declaringType, HttpStatus.BAD_REQUEST).orElse(null); - } - if (errorRoute == null) { - // handle error with a method that is global with bad request - errorRoute = router.route(HttpStatus.BAD_REQUEST).orElse(null); - } - } - - // any another other exception may arise. handle these with non global exception marked method or a global exception marked method. - if (errorRoute == null) { - if (declaringType != null) { - errorRoute = router.route(declaringType, cause).orElse(null); - } - if (errorRoute == null) { - errorRoute = router.route(cause).orElse(null); - } - } - - if (errorRoute != null) { - if (LOG.isErrorEnabled()) { - LOG.error("Unexpected error occurred: " + cause.getMessage(), cause); - } - if (LOG.isDebugEnabled()) { - LOG.debug("Found matching exception handler for exception [{}]: {}", cause.getMessage(), errorRoute); - } - errorRoute = requestArgumentSatisfier.fulfillArgumentRequirements(errorRoute, httpRequest, false); - MediaType defaultResponseMediaType = errorRoute.getProduces().stream().findFirst().orElse(MediaType.APPLICATION_JSON_TYPE); - try { - Object result = errorRoute.execute(); - io.micronaut.http.MutableHttpResponse response = errorResultToResponse(result); - MethodBasedRouteMatch methodBasedRoute = (MethodBasedRouteMatch) errorRoute; - AtomicReference> requestReference = new AtomicReference<>(httpRequest); - Flowable> routePublisher = buildRoutePublisher( - methodBasedRoute.getDeclaringType(), - methodBasedRoute.getReturnType().getType(), - requestReference, - Flowable.just(response)); - - Flowable> filteredPublisher = filterPublisher( - requestReference, - routePublisher, - ioExecutor); - - return subscribeToResponsePublisher( - defaultResponseMediaType, - filteredPublisher - ); - - } catch (Throwable e) { - if (LOG.isErrorEnabled()) { - LOG.error("Exception occurred executing error handler. Falling back to default error handling: " + e.getMessage(), e); - } - return writeDefaultErrorResponse(e); - } - } else { - Optional exceptionHandler = beanLocator - .findBean(ExceptionHandler.class, Qualifiers.byTypeArguments(cause.getClass(), Object.class)); - - if (exceptionHandler.isPresent()) { - ExceptionHandler handler = exceptionHandler.get(); - MediaType defaultResponseMediaType = MediaType.fromType(exceptionHandler.getClass()).orElse(MediaType.APPLICATION_JSON_TYPE); - try { - Object result = handler.handle(httpRequest, cause); - AtomicReference> requestReference = new AtomicReference<>(httpRequest); - io.micronaut.http.MutableHttpResponse response = errorResultToResponse(result); - Flowable> routePublisher = buildRoutePublisher( - handler.getClass(), - result != null ? result.getClass() : HttpResponse.class, - requestReference, - Flowable.just(response)); - - Flowable> filteredPublisher = filterPublisher( - requestReference, - routePublisher, - ioExecutor); - - return subscribeToResponsePublisher( - defaultResponseMediaType, - filteredPublisher - ); - } catch (Throwable e) { - if (LOG.isDebugEnabled()) { - LOG.debug("Exception occurred executing error handler. Falling back to default error handling."); - } - return writeDefaultErrorResponse(e); - } - } else { - return writeDefaultErrorResponse(cause); - } - } - } - - public HttpResponse handleRequest(io.micronaut.http.HttpRequest request) { - try { - return handleRequestInternal(request).blockingFirst(); - } catch (Throwable throwable) { - return exceptionCaught(request, (RouteMatch) request.getAttribute(HttpAttributes.ROUTE_MATCH).get(), throwable).blockingFirst(); - } - } - - private Flowable handleRequestInternal(io.micronaut.http.HttpRequest request) { - io.micronaut.http.HttpMethod httpMethod = request.getMethod(); - String requestPath = request.getPath(); - - if (LOG.isDebugEnabled()) { - LOG.debug("Matching route {} - {}", httpMethod, requestPath); - } - - Optional> routeMatch = Optional.empty(); - - List> uriRoutes = router - .find(httpMethod, requestPath) - .filter(match -> match.test(request)) - .collect(StreamUtils.minAll( - Comparator.comparingInt(match -> match.getVariableValues().size()), - Collectors.toList())); - - if (uriRoutes.size() > 1) { - throw new DuplicateRouteException(requestPath, uriRoutes); - } else if (uriRoutes.size() == 1) { - UriRouteMatch establishedRoute = uriRoutes.get(0); - request.setAttribute(HttpAttributes.ROUTE, establishedRoute.getRoute()); - request.setAttribute(HttpAttributes.ROUTE_MATCH, establishedRoute); - request.setAttribute(HttpAttributes.URI_TEMPLATE, establishedRoute.getRoute().getUriMatchTemplate().toString()); - routeMatch = Optional.of(establishedRoute); - } - - RouteMatch route; - - if (!routeMatch.isPresent()) { - if (LOG.isDebugEnabled()) { - LOG.debug("No matching route found for URI {} and method {}", request.getPath(), httpMethod); - } - - // if there is no route present try to locate a route that matches a different HTTP method - Set existingRoutes = router - .findAny(request.getPath()) - .map(UriRouteMatch::getHttpMethod) - .collect(Collectors.toSet()); - - if (!existingRoutes.isEmpty()) { - if (LOG.isDebugEnabled()) { - LOG.debug("Method not allowed for URI {} and method {}", request.getPath(), httpMethod); - } - - return handleStatusError( - request, - HttpResponse.notAllowed(existingRoutes), - "Method [" + httpMethod + "] not allowed. Allowed methods: " + existingRoutes); - } else { - Optional optionalFile = matchFile(requestPath); - - if (optionalFile.isPresent()) { - route = new BasicObjectRouteMatch(optionalFile.get()); - } else { - Optional> statusRoute = router.route(HttpStatus.NOT_FOUND); - if (statusRoute.isPresent()) { - route = statusRoute.get(); - } else { - return emitDefaultNotFoundResponse(request); - } - } - } - } else { - route = routeMatch.get(); - } - // Check that the route is an accepted content type - MediaType contentType = request.getContentType().orElse(null); - if (!route.accept(contentType)) { - if (LOG.isDebugEnabled()) { - LOG.debug("Matched route is not a supported media type: {}", contentType); - } - - return handleStatusError( - request, - HttpResponse.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE), - "Unsupported Media Type: " + contentType); - } - if (LOG.isDebugEnabled()) { - if (route instanceof MethodBasedRouteMatch) { - LOG.debug("Matched route {} - {} to controller {}", httpMethod, requestPath, route.getDeclaringType()); - } else { - LOG.debug("Matched route {} - {}", httpMethod, requestPath); - } - } - // all ok proceed to try and execute the route - return handleRouteMatch(route, request); - } - - private Flowable handleStatusError( - HttpRequest request, - MutableHttpResponse defaultResponse, - String message) { - Optional> statusRoute = router.route(defaultResponse.status()); - if (statusRoute.isPresent()) { - RouteMatch routeMatch = statusRoute.get(); - return handleRouteMatch(routeMatch, request); - } else { - - if (HttpMethod.permitsRequestBody(request.getMethod())) { - JsonError error = newError(request, message); - defaultResponse.body(error); - } - - - AtomicReference> requestReference = new AtomicReference<>(request); - Flowable> responsePublisher = filterPublisher( - requestReference, - Flowable.just(defaultResponse), - ioExecutor - ); - return subscribeToResponsePublisher( - MediaType.APPLICATION_JSON_TYPE, - responsePublisher - ); - } - } - - private Optional matchFile(String path) { - Optional optionalUrl = staticResourceResolver.resolve(path); - - if (optionalUrl.isPresent()) { - try { - URL url = optionalUrl.get(); - if (url.getProtocol().equals("file")) { - File file = Paths.get(url.toURI()).toFile(); - if (file.exists() && !file.isDirectory() && file.canRead()) { - return Optional.of(new BasicSystemFileCustomizableResponseType(file)); - } - } - - return Optional.empty(); - } catch (URISyntaxException e) { - //no-op - } - } - - return Optional.empty(); - } - - private Flowable emitDefaultNotFoundResponse(io.micronaut.http.HttpRequest request) { - MutableHttpResponse res = newNotFoundError(request); - AtomicReference> requestReference = new AtomicReference<>(request); - Flowable> responsePublisher = filterPublisher( - requestReference, - Flowable.just(res), - ioExecutor - ); - return subscribeToResponsePublisher( - MediaType.APPLICATION_JSON_TYPE, - responsePublisher - ); - } - - private MutableHttpResponse newNotFoundError(HttpRequest request) { - JsonError error = newError(request, "Page Not Found"); - return HttpResponse.notFound() - .body(error); - } - - private JsonError newError(io.micronaut.http.HttpRequest request, String message) { - URI uri = request.getUri(); - return new JsonError(message) - .link(Link.SELF, Link.of(uri)); - } - - private MutableHttpResponse errorResultToResponse(Object result) { - MutableHttpResponse response; - if (result == null) { - response = HttpResponse.serverError(); - } else if (result instanceof io.micronaut.http.HttpResponse) { - response = (MutableHttpResponse) result; - } else { - response = HttpResponse.serverError() - .body(result); - MediaType.fromType(result.getClass()).ifPresent(response::contentType); - } - return response; - } - - private Flowable handleRouteMatch( - RouteMatch route, - HttpRequest request - ) { - // try to fulfill the argument requirements of the route - route = requestArgumentSatisfier.fulfillArgumentRequirements(route, request, false); - - try { - AtomicReference> flowableReference = new AtomicReference<>(); - route = prepareRouteForExecution(route, request, flowableReference); - route.execute(); - return flowableReference.get(); - } catch (Exception e) { - return exceptionCaught(request, route, e); - } - } - - private RouteMatch prepareRouteForExecution(RouteMatch route, HttpRequest request, AtomicReference> reference) { - // Select the most appropriate Executor - ExecutorService executor; - if (route instanceof MethodBasedRouteMatch) { - executor = executorSelector.select((MethodBasedRouteMatch) route).orElse(ioExecutor); - } else { - executor = ioExecutor; - } - - route = route.decorate(finalRoute -> { - MediaType defaultResponseMediaType = finalRoute - .getProduces() - .stream() - .findFirst() - .orElse(MediaType.APPLICATION_JSON_TYPE); - - - ReturnType genericReturnType = finalRoute.getReturnType(); - Class javaReturnType = genericReturnType.getType(); - - AtomicReference> requestReference = new AtomicReference<>(request); - boolean isFuture = CompletableFuture.class.isAssignableFrom(javaReturnType); - boolean isReactiveReturnType = Publishers.isConvertibleToPublisher(javaReturnType) || isFuture; - boolean isSingle = - isReactiveReturnType && Publishers.isSingle(javaReturnType) - || isResponsePublisher(genericReturnType, javaReturnType) - || isFuture - || finalRoute.getAnnotationMetadata().getValue(Produces.class, "single", Boolean.class).orElse(false); - - // build the result emitter. This result emitter emits the response from a controller action - Flowable resultEmitter = buildResultEmitter(finalRoute, requestReference, isReactiveReturnType, isSingle); - - - // here we transform the result of the controller action into a MutableHttpResponse - Flowable> routePublisher = resultEmitter.map((message) -> { - HttpResponse response = messageToResponse(finalRoute, message); - MutableHttpResponse finalResponse = (MutableHttpResponse) response; - HttpStatus status = finalResponse.getStatus(); - if (status.getCode() >= HttpStatus.BAD_REQUEST.getCode()) { - Class declaringType = ((MethodBasedRouteMatch) finalRoute).getDeclaringType(); - // handle re-mapping of errors - Optional> statusRoute = Optional.empty(); - // if declaringType is not null, this means its a locally marked method handler - if (declaringType != null) { - statusRoute = router.route(declaringType, status); - } - if (!statusRoute.isPresent()) { - statusRoute = router.route(status); - } - io.micronaut.http.HttpRequest httpRequest = requestReference.get(); - - if (statusRoute.isPresent()) { - RouteMatch newRoute = statusRoute.get(); - requestArgumentSatisfier.fulfillArgumentRequirements(newRoute, httpRequest, true); - - if (newRoute.isExecutable()) { - Object result; - try { - result = newRoute.execute(); - finalResponse = messageToResponse(newRoute, result); - } catch (Throwable e) { - throw new InternalServerException("Error executing status route [" + newRoute + "]: " + e.getMessage(), e); - } - } - } - - } - return finalResponse; - }); - - routePublisher = buildRoutePublisher( - finalRoute.getDeclaringType(), - javaReturnType, - requestReference, - routePublisher - ); - - // process the publisher through the available filters - Flowable> filteredPublisher = filterPublisher( - requestReference, - routePublisher, - executor - ); - - boolean isStreaming = isReactiveReturnType && !isSingle; - - filteredPublisher = filteredPublisher.switchMap((response) -> { - Optional responseBody = response.getBody(); - if (responseBody.isPresent()) { - Object body = responseBody.get(); - if (isStreaming) { - // handled downstream - return Flowable.just(response); - } else if (Publishers.isConvertibleToPublisher(body)) { - Flowable bodyFlowable = Publishers.convertPublisher(body, Flowable.class); - Flowable> bodyToResponse = bodyFlowable.map((bodyContent) -> - setBodyContent(response, bodyContent) - ); - return bodyToResponse.switchIfEmpty(Flowable.just(response)); - } - } - - return Flowable.just(response); - }); - - reference.set(subscribeToResponsePublisher(defaultResponseMediaType, filteredPublisher)); - - return null; - }); - return route; - } - - private Flowable> buildRoutePublisher( - Class declaringType, - Class javaReturnType, - AtomicReference> requestReference, - Flowable> routePublisher) { - // In the case of an empty reactive type we switch handling so that - // a 404 NOT_FOUND is returned - routePublisher = routePublisher.switchIfEmpty(Flowable.create((emitter) -> { - HttpRequest httpRequest = requestReference.get(); - MutableHttpResponse response; - if (javaReturnType != void.class) { - - // handle re-mapping of errors - Optional> statusRoute = Optional.empty(); - // if declaringType is not null, this means its a locally marked method handler - if (declaringType != null) { - statusRoute = router.route(declaringType, HttpStatus.NOT_FOUND); - } - if (!statusRoute.isPresent()) { - statusRoute = router.route(HttpStatus.NOT_FOUND); - } - - if (statusRoute.isPresent()) { - RouteMatch newRoute = statusRoute.get(); - requestArgumentSatisfier.fulfillArgumentRequirements(newRoute, httpRequest, true); - - if (newRoute.isExecutable()) { - try { - Object result = newRoute.execute(); - response = messageToResponse(newRoute, result); - } catch (Throwable e) { - emitter.onError(new InternalServerException("Error executing status route [" + newRoute + "]: " + e.getMessage(), e)); - return; - } - - } else { - response = newNotFoundError(httpRequest); - } - } else { - response = newNotFoundError(httpRequest); - } - } else { - // void return type with no response, nothing else to do - response = HttpResponse.ok(); - } - try { - emitter.onNext(response); - emitter.onComplete(); - } catch (Throwable e) { - emitter.onError(new InternalServerException("Error executing Error route [" + response.getStatus() + "]: " + e.getMessage(), e)); - } - }, BackpressureStrategy.ERROR)); - return routePublisher; - } - - private Flowable subscribeToResponsePublisher( - MediaType defaultResponseMediaType, - Flowable> finalPublisher) { - finalPublisher = finalPublisher.map((response) -> { - Optional specifiedMediaType = response.getContentType(); - MediaType responseMediaType = specifiedMediaType.orElse(defaultResponseMediaType); - - Optional responseBody = response.getBody(); - if (responseBody.isPresent()) { - - Object body = responseBody.get(); - - if (specifiedMediaType.isPresent()) { - - Optional registeredCodec = mediaTypeCodecRegistry.findCodec(responseMediaType, body.getClass()); - if (registeredCodec.isPresent()) { - MediaTypeCodec codec = registeredCodec.get(); - return encodeBodyWithCodec(response, body, codec, responseMediaType); - } - } - - Optional registeredCodec = mediaTypeCodecRegistry.findCodec(defaultResponseMediaType, body.getClass()); - if (registeredCodec.isPresent()) { - MediaTypeCodec codec = registeredCodec.get(); - return encodeBodyWithCodec(response, body, codec, responseMediaType); - } - - MediaTypeCodec defaultCodec = new TextPlainCodec(Charset.defaultCharset()); - - return encodeBodyWithCodec(response, body, defaultCodec, responseMediaType); - } else { - return response; - } - }); - - return finalPublisher; - } - - private MutableHttpResponse encodeBodyWithCodec(MutableHttpResponse response, Object body, MediaTypeCodec codec, MediaType mediaType) { - ByteBuf byteBuf = encodeBodyAsByteBuf(body, codec); - int len = byteBuf.readableBytes(); - MutableHttpHeaders headers = response.getHeaders(); - if (!headers.contains(HttpHeaders.CONTENT_TYPE)) { - headers.add(HttpHeaders.CONTENT_TYPE, mediaType); - } - headers.remove(HttpHeaders.CONTENT_LENGTH); - headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(len)); - - setBodyContent(response, byteBuf); - return response; - } - - private MutableHttpResponse setBodyContent(MutableHttpResponse response, Object bodyContent) { - @SuppressWarnings("unchecked") - MutableHttpResponse res = response.body(bodyContent); - return res; - } - - private ByteBuf encodeBodyAsByteBuf(Object body, MediaTypeCodec codec) { - ByteBuf byteBuf; - if (body instanceof ByteBuf) { - byteBuf = (ByteBuf) body; - } else if (body instanceof ByteBuffer) { - ByteBuffer byteBuffer = (ByteBuffer) body; - Object nativeBuffer = byteBuffer.asNativeBuffer(); - if (nativeBuffer instanceof ByteBuf) { - byteBuf = (ByteBuf) nativeBuffer; - } else { - byteBuf = Unpooled.copiedBuffer(byteBuffer.asNioBuffer()); - } - } else if (body instanceof byte[]) { - byteBuf = Unpooled.copiedBuffer((byte[]) body); - } else { - byteBuf = (ByteBuf) codec.encode(body, new NettyByteBufferFactory(ByteBufAllocator.DEFAULT)).asNativeBuffer(); - } - return byteBuf; - } - - // builds the result emitter for a given route action - private Flowable buildResultEmitter( - RouteMatch finalRoute, - AtomicReference> requestReference, - boolean isReactiveReturnType, - boolean isSingleResult) { - Flowable resultEmitter; - if (isReactiveReturnType) { - // if the return type is reactive, execute the action and obtain the Observable - RouteMatch routeMatch = finalRoute; - if (!routeMatch.isExecutable()) { - routeMatch = requestArgumentSatisfier.fulfillArgumentRequirements(routeMatch, requestReference.get(), true); - } - try { - if (isSingleResult) { - // for a single result we are fine as is - resultEmitter = Flowable.defer(() -> { - Object result = finalRoute.execute(); - return Publishers.convertPublisher(result, Publisher.class); - }); - } else { - // for a streaming response we wrap the result on an HttpResponse so that a single result is received - // then the result can be streamed chunk by chunk - resultEmitter = Flowable.create((emitter) -> { - Object result = finalRoute.execute(); - MutableHttpResponse chunkedResponse = HttpResponse.ok(result); - chunkedResponse.header(HttpHeaders.TRANSFER_ENCODING, "chunked"); - emitter.onNext(chunkedResponse); - emitter.onComplete(); - // should be no back pressure - }, BackpressureStrategy.ERROR); - } - } catch (Throwable e) { - resultEmitter = Flowable.error(new InternalServerException("Error executing route [" + routeMatch + "]: " + e.getMessage(), e)); - } - } else { - // for non-reactive results we build flowable that executes the - // route - resultEmitter = Flowable.create((emitter) -> { - HttpRequest httpRequest = requestReference.get(); - RouteMatch routeMatch = finalRoute; - if (!routeMatch.isExecutable()) { - routeMatch = requestArgumentSatisfier.fulfillArgumentRequirements(routeMatch, httpRequest, true); - } - Object result; - try { - result = routeMatch.execute(); - } catch (Throwable e) { - emitter.onError(e); - return; - } - - if (result == null) { - // empty flowable - emitter.onComplete(); - } else { - // emit the result - emitter.onNext(result); - emitter.onComplete(); - } - - // should be no back pressure - }, BackpressureStrategy.ERROR); - } - return resultEmitter; - } - - private MutableHttpResponse messageToResponse(RouteMatch finalRoute, Object message) { - MutableHttpResponse response; - if (message instanceof HttpResponse) { - response = ConversionService.SHARED.convert(message, MutableHttpResponse.class) - .orElseThrow(() -> new InternalServerException("Emitted response is not mutable")); - } else { - if (message instanceof HttpStatus) { - response = HttpResponse.status((HttpStatus) message); - } else { - HttpStatus status = HttpStatus.OK; - - if (finalRoute instanceof MethodBasedRouteMatch) { - final MethodBasedRouteMatch rm = (MethodBasedRouteMatch) finalRoute; - if (rm.hasAnnotation(Status.class)) { - status = rm.getAnnotation(Status.class).getValue(HttpStatus.class).get(); - } - } - response = HttpResponse.status(status).body(message); - } - } - return response; - } - - private boolean isResponsePublisher(ReturnType genericReturnType, Class javaReturnType) { - return Publishers.isConvertibleToPublisher(javaReturnType) && genericReturnType.getFirstTypeVariable().map(arg -> HttpResponse.class.isAssignableFrom(arg.getType())).orElse(false); - } - - private Flowable> filterPublisher( - AtomicReference> requestReference, - Publisher> routePublisher, ExecutorService executor) { - Publisher> finalPublisher; - List filters = new ArrayList<>(router.findFilters(requestReference.get())); - if (!filters.isEmpty()) { - // make the action executor the last filter in the chain - filters.add((HttpServerFilter) (req, chain) -> routePublisher); - - AtomicInteger integer = new AtomicInteger(); - int len = filters.size(); - ServerFilterChain filterChain = new ServerFilterChain() { - @SuppressWarnings("unchecked") - @Override - public Publisher> proceed(io.micronaut.http.HttpRequest request) { - int pos = integer.incrementAndGet(); - if (pos > len) { - throw new IllegalStateException("The FilterChain.proceed(..) method should be invoked exactly once per filter execution. The method has instead been invoked multiple times by an erroneous filter definition."); - } - HttpFilter httpFilter = filters.get(pos); - return (Publisher>) httpFilter.doFilter(requestReference.getAndSet(request), this); - } - }; - HttpFilter httpFilter = filters.get(0); - Publisher> resultingPublisher = httpFilter.doFilter(requestReference.get(), filterChain); - finalPublisher = (Publisher>) resultingPublisher; - } else { - finalPublisher = routePublisher; - } - - // Handle the scheduler to subscribe on - if (finalPublisher instanceof Flowable) { - return ((Flowable>) finalPublisher) - .subscribeOn(Schedulers.from(executor)); - } else { - return Flowable.fromPublisher(finalPublisher) - .subscribeOn(Schedulers.from(executor)); - } - } - - @SuppressWarnings("unchecked") - private Flowable writeDefaultErrorResponse(Throwable cause) { - if (LOG.isErrorEnabled()) { - LOG.error("Unexpected error occurred: " + cause.getMessage(), cause); - } - - MutableHttpResponse error = HttpResponse.serverError() - .body(new JsonError("Internal Server Error: " + cause.getMessage())); - return subscribeToResponsePublisher( - MediaType.APPLICATION_JSON_TYPE, - Flowable.just(error) - ); - } -} diff --git a/micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicSystemFileCustomizableResponseType.java b/micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicSystemFileCustomizableResponseType.java deleted file mode 100644 index b0459bc..0000000 --- a/micronaut-http-server-basic/src/main/groovy/com/agorapulse/micronaut/http/basic/BasicSystemFileCustomizableResponseType.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.agorapulse.micronaut.http.basic; - -import io.micronaut.http.MutableHttpResponse; -import io.micronaut.http.server.types.CustomizableResponseTypeException; -import io.micronaut.http.server.types.files.SystemFileCustomizableResponseType; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.Optional; - -/** - * Writes a {@link File} to the Netty context. - * - * @since 1.0 - */ -public class BasicSystemFileCustomizableResponseType extends SystemFileCustomizableResponseType { - - private static final int LENGTH_8K = 8192; - - protected final RandomAccessFile raf; - protected final long rafLength; - protected Optional delegate = Optional.empty(); - - /** - * @param file The file - */ - public BasicSystemFileCustomizableResponseType(File file) { - super(file); - try { - this.raf = new RandomAccessFile(file, "r"); - } catch (FileNotFoundException e) { - throw new CustomizableResponseTypeException("Could not find file", e); - } - try { - this.rafLength = raf.length(); - } catch (IOException e) { - throw new CustomizableResponseTypeException("Could not determine file length", e); - } - } - - /** - * @param delegate The system file customizable response type - */ - public BasicSystemFileCustomizableResponseType(SystemFileCustomizableResponseType delegate) { - this(delegate.getFile()); - this.delegate = Optional.of(delegate); - } - - @Override - public long getLength() { - return rafLength; - } - - @Override - public long getLastModified() { - return delegate.map(SystemFileCustomizableResponseType::getLastModified).orElse(super.getLastModified()); - } - - @Override - public String getName() { - return delegate.map(SystemFileCustomizableResponseType::getName).orElse(super.getName()); - } - - /** - * @param response The response to modify - */ - public void process(MutableHttpResponse response) { - response.header(io.micronaut.http.HttpHeaders.CONTENT_LENGTH, String.valueOf(getLength())); - delegate.ifPresent((type) -> type.process(response)); - } - -// @Override -// public void write(HttpRequest request, MutableHttpResponse response, ChannelHandlerContext context) { -// -// if (response instanceof NettyHttpResponse) { -// -// FullHttpResponse nettyResponse = ((NettyHttpResponse) response).getNativeResponse(); -// -// //The streams codec prevents non full responses from being written -// Optional -// .ofNullable(context.pipeline().get(NettyHttpServer.HTTP_STREAMS_CODEC)) -// .ifPresent(handler -> context.pipeline().replace(handler, "chunked-handler", new ChunkedWriteHandler())); -// -// // Write the request data -// HttpHeaders headers = nettyResponse.headers(); -// context.write(new DefaultHttpResponse(nettyResponse.protocolVersion(), nettyResponse.status(), headers), context.voidPromise()); -// -// // Write the content. -// ChannelFuture flushFuture; -// if (context.pipeline().get(SslHandler.class) == null && SmartHttpContentCompressor.shouldSkip(headers)) { -// // SSL not enabled - can use zero-copy file transfer. -// // Remove the content compressor to prevent incorrect behavior with zero-copy -// HttpContentCompressor compressor = context.pipeline().get(HttpContentCompressor.class); -// if (compressor != null) { -// context.pipeline().remove(HttpContentCompressor.class); -// } -// -// context.write(new DefaultFileRegion(raf.getChannel(), 0, getLength()), context.newProgressivePromise()); -// flushFuture = context.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT); -// } else { -// // SSL enabled - cannot use zero-copy file transfer. -// try { -// // HttpChunkedInput will write the end marker (LastHttpContent) for us. -// flushFuture = context.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, getLength(), LENGTH_8K)), -// context.newProgressivePromise()); -// } catch (IOException e) { -// throw new CustomizableResponseTypeException("Could not read file", e); -// } -// } -// -// flushFuture.addListener(new DefaultCloseHandler(context, request, response.code())); -// } else { -// throw new IllegalArgumentException("Unsupported response type. Not a Netty response: " + response); -// } -// } -} diff --git a/micronaut-http-server-tck-netty-tests/build.gradle b/micronaut-http-server-tck-netty-tests/build.gradle deleted file mode 100644 index 0bb7cd0..0000000 --- a/micronaut-http-server-tck-netty-tests/build.gradle +++ /dev/null @@ -1,7 +0,0 @@ -dependencies { - testCompile project(':micronaut-http-server-tck') - - // custom test - testCompile "com.agorapulse:gru-http:$gruVersion" - testCompile "io.micronaut:micronaut-http-server-netty" -} diff --git a/micronaut-http-server-tck-netty-tests/src/test/groovy/com/agorapulse/micronaut/http/server/tck/netty/tests/NettyHttpServerSpec.groovy b/micronaut-http-server-tck-netty-tests/src/test/groovy/com/agorapulse/micronaut/http/server/tck/netty/tests/NettyHttpServerSpec.groovy deleted file mode 100644 index 3ab1f51..0000000 --- a/micronaut-http-server-tck-netty-tests/src/test/groovy/com/agorapulse/micronaut/http/server/tck/netty/tests/NettyHttpServerSpec.groovy +++ /dev/null @@ -1,28 +0,0 @@ -package com.agorapulse.micronaut.http.server.tck.netty.tests - -import com.agorapulse.gru.Gru -import com.agorapulse.gru.http.Http -import com.agorapulse.micronaut.http.server.tck.AbstractApiGatewayProxyHttpRequestSpec -import io.micronaut.context.ApplicationContext -import io.micronaut.runtime.server.EmbeddedServer -import org.junit.Rule -import spock.lang.AutoCleanup -import spock.lang.Shared - -/** - * Test for basic http server using Netty implementation. - */ -class NettyHttpServerSpec extends AbstractApiGatewayProxyHttpRequestSpec { - - @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) - - @Rule Gru gru = Gru.equip(Http.steal(this)) - - void setup() { - String serverUrl = embeddedServer.URL - gru.prepare { - baseUri serverUrl - } - } - -} diff --git a/micronaut-http-server-tck/build.gradle b/micronaut-http-server-tck/build.gradle deleted file mode 100644 index 0acc515..0000000 --- a/micronaut-http-server-tck/build.gradle +++ /dev/null @@ -1,5 +0,0 @@ -dependencies { - compile "io.micronaut:micronaut-http-server" - compile "io.micronaut:micronaut-router" - compile "com.agorapulse:gru:$gruVersion" -} diff --git a/micronaut-http-server-tck/src/main/groovy/com/agorapulse/micronaut/http/server/tck/AbstractApiGatewayProxyHttpRequestSpec.groovy b/micronaut-http-server-tck/src/main/groovy/com/agorapulse/micronaut/http/server/tck/AbstractApiGatewayProxyHttpRequestSpec.groovy deleted file mode 100644 index 2104bd8..0000000 --- a/micronaut-http-server-tck/src/main/groovy/com/agorapulse/micronaut/http/server/tck/AbstractApiGatewayProxyHttpRequestSpec.groovy +++ /dev/null @@ -1,69 +0,0 @@ -package com.agorapulse.micronaut.http.server.tck - -import com.agorapulse.gru.Gru -import spock.lang.Specification - -/** - * Base class for api gateway proxy request tests. - */ -abstract class AbstractApiGatewayProxyHttpRequestSpec extends Specification { - - abstract Gru getGru() - - void 'should return hello'() { - expect: - gru.test { - get '/hello' - expect { - text inline('Hello Galaxy!') - } - } - } - - void 'should return method not allowed'() { - expect: - gru.test { - post '/hello' - expect { - status METHOD_NOT_ALLOWED - } - } - } - - void 'should transform object to json'() { - expect: - gru.test { - get '/hello/greet/hello/en' - expect { - json inline('{ "message" : "hello", "language" : "en" }') - } - } - } - - void 'transform body into object and returns status'() { - expect: - gru.test { - post '/hello/greet', { - headers 'Content-Type': 'application/json' - json inline('{ "message" : "hello", "language" : "en" }') - } - expect { - status CREATED - json inline('{ "message" : "hello", "language" : "en" }') - } - } - } - - void 'optional int parameter from body'() { - expect: - gru.test { - put '/hello/mfa', { - json inline('{"enable": true }') - } - expect { - status BAD_REQUEST - } - } - } - -} diff --git a/micronaut-http-server-tck/src/main/groovy/hello/galaxy/Greetings.groovy b/micronaut-http-server-tck/src/main/groovy/hello/galaxy/Greetings.groovy deleted file mode 100644 index 3799b73..0000000 --- a/micronaut-http-server-tck/src/main/groovy/hello/galaxy/Greetings.groovy +++ /dev/null @@ -1,14 +0,0 @@ -package hello.galaxy - -import groovy.transform.CompileStatic - -/** - * Sample object. - */ -@CompileStatic -class Greetings { - - String message - String language - -} diff --git a/micronaut-http-server-tck/src/main/groovy/hello/galaxy/HelloController.groovy b/micronaut-http-server-tck/src/main/groovy/hello/galaxy/HelloController.groovy deleted file mode 100644 index 97e41d4..0000000 --- a/micronaut-http-server-tck/src/main/groovy/hello/galaxy/HelloController.groovy +++ /dev/null @@ -1,43 +0,0 @@ -package hello.galaxy - -import groovy.transform.CompileStatic -import io.micronaut.http.HttpResponse -import io.micronaut.http.annotation.Body -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Get -import io.micronaut.http.annotation.Post -import io.micronaut.http.annotation.Put - -/** - * Sample controller. - */ -@CompileStatic -@Controller('/hello') -class HelloController { - @Get('/') - String index() { - return 'Hello Galaxy!' - } - - @Post('/greet') - HttpResponse newGreeting(@Body Greetings body) { - return HttpResponse.created(body) - } - - @Get('/greet/{message}/{language}') - Greetings greet(String message, String language) { - return new Greetings(message: message, language: language) - } - - @Put('/mfa') - HttpResponse mfa(Optional enable, Optional multiFactorCode) { - if (!multiFactorCode.present) { - return HttpResponse.badRequest() - } - if (enable.present && enable.get()) { - int code = multiFactorCode.get() - return HttpResponse.ok([enable: true, mfa: code]) - } - return HttpResponse.ok([enable: false]) - } -} diff --git a/settings.gradle b/settings.gradle index 6829c3b..3429e27 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,11 +1,7 @@ rootProject.name = 'micronaut-libraries' include 'micronaut-aws-sdk' -include 'micronaut-function-aws-agp' include 'micronaut-grails' -include 'micronaut-http-server-basic' -include 'micronaut-http-server-tck' -include 'micronaut-http-server-tck-netty-tests' // examples include 'examples/local-server' From f27310558e7df22baf6c695445622c7531f1e387 Mon Sep 17 00:00:00 2001 From: Vladimir Orany Date: Fri, 31 May 2019 12:33:20 +0200 Subject: [PATCH 2/4] fixed checkstyle violation --- .../micronaut/http/examples/planets/MicronautHandler.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java index 11ecb7f..727275c 100644 --- a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java +++ b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/MicronautHandler.java @@ -6,7 +6,6 @@ import io.micronaut.context.ApplicationContext; import io.micronaut.context.ApplicationContextBuilder; import io.micronaut.function.aws.proxy.MicronautLambdaContainerHandler; -import io.micronaut.http.HttpRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,8 +14,6 @@ import java.io.OutputStream; import java.util.function.Consumer; -import static com.amazonaws.serverless.proxy.RequestReader.LAMBDA_CONTEXT_PROPERTY; - public class MicronautHandler implements RequestStreamHandler { private static final Logger LOGGER = LoggerFactory.getLogger(MicronautHandler.class); From 2586708e27ce125741ddadbbbb5b1db732fcefa7 Mon Sep 17 00:00:00 2001 From: Vladimir Orany Date: Fri, 31 May 2019 12:41:33 +0200 Subject: [PATCH 3/4] fixed codenarc violation --- .../http/examples/planets/PlanetController.groovy | 4 ++++ .../http/examples/spacecrafts/SpacecraftController.groovy | 7 +++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy index 36ef367..dfd3301 100644 --- a/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy +++ b/examples/planets/src/main/groovy/com/agorapulse/micronaut/http/examples/planets/PlanetController.groovy @@ -50,6 +50,10 @@ class PlanetController { @Status(HttpStatus.NOT_FOUND) @Error(PlanetNotFoundException) + @SuppressWarnings([ + 'EmptyMethod', + 'UnusedMethodParameter', + ]) void planetNotFound(PlanetNotFoundException ex) { } } diff --git a/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy index b0880f4..053c3ff 100644 --- a/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy +++ b/examples/spacecrafts/src/main/groovy/com/agorapulse/micronaut/http/examples/spacecrafts/SpacecraftController.groovy @@ -52,9 +52,12 @@ class SpacecraftController { return planet } - @Status(HttpStatus.NOT_FOUND) @Error(SpacecraftNotFoundException) - void spacecraftNotFound(SpacecraftNotFoundException ex) {} + @SuppressWarnings([ + 'EmptyMethod', + 'UnusedMethodParameter', + ]) + void spacecraftNotFound(SpacecraftNotFoundException ex) { } } From 08d21b396f9668bdb2d1154214468787835e94f5 Mon Sep 17 00:00:00 2001 From: Vladimir Orany Date: Fri, 31 May 2019 14:29:52 +0200 Subject: [PATCH 4/4] fixed AWS SDK version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index a62613a..e2b89b6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ gruVersion = 0.8.1 druVersion = 0.6.0 groovyVersion = 2.5.7 spockVersion = 1.3-groovy-2.5 -awsSdkVersion = 1.11.256 +awsSdkVersion = 1.11.562 testcontainersVersion = 1.11.3 # this should be aligned to Micronaut version