diff --git a/README.adoc b/README.adoc index 595bac1d8..2ec9c385b 100644 --- a/README.adoc +++ b/README.adoc @@ -62,7 +62,6 @@ endpoints and/or message stream listeners/publishers with RabbitMQ, Kafka etc. * _Packaging functions for deployments, specific to the target platform (e.g., Project Riff, AWS Lambda and more)_ * _Adapters to expose function to the outside world as HTTP endpoints etc._ * _Deploying a JAR file containing such an application context with an isolated classloader, so that you can pack them together in a single JVM._ -* _Compiling strings which are Java function bodies into bytecode, and then turning them into `@Beans` that can be wrapped as above._ * _Adapters for https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-aws[AWS Lambda], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[Azure], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp[Google Cloud Functions], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk[Apache OpenWhisk] and possibly other "serverless" service providers._ == Getting Started @@ -185,7 +184,7 @@ from the `file` menu. == Contributing -:spring-cloud-build-branch: master +:spring-cloud-build-branch: 3.1.x Spring Cloud is released under the non-restrictive Apache 2.0 license, and follows a very standard Github development process, using Github @@ -202,7 +201,7 @@ author credit if we do. Active contributors might be asked to join the core tea given the ability to merge pull requests. === Code of Conduct -This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc[code of +This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/3.1.x/docs/src/main/asciidoc/code-of-conduct.adoc[code of conduct]. By participating, you are expected to uphold this code. Please report unacceptable behavior to spring-code-of-conduct@pivotal.io. @@ -213,7 +212,7 @@ added after the original pull request but before a merge. * Use the Spring Framework code format conventions. If you use Eclipse you can import formatter settings using the `eclipse-code-formatter.xml` file from the - https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring + https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring Cloud Build] project. If using IntelliJ, you can use the https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter Plugin] to import the same file. @@ -309,7 +308,7 @@ If you need to suppress some rules (e.g. line length needs to be longer), then i It's advisable to copy the `${spring-cloud-build.rootFolder}/.editorconfig` and `${spring-cloud-build.rootFolder}/.springformat` to your project. That way, some default formatting rules will be applied. You can do so by running this script: ```bash -$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig +$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/.editorconfig -o .editorconfig $ touch .springformat ``` @@ -318,7 +317,7 @@ $ touch .springformat ==== Intellij IDEA In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin. -The following files can be found in the https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools[Spring Cloud Build] project. +The following files can be found in the https://github.com/spring-cloud/spring-cloud-build/tree/3.1.x/spring-cloud-build-tools[Spring Cloud Build] project. .spring-cloud-build-tools/ ---- @@ -357,10 +356,10 @@ To have Intellij work with Checkstyle, you have to install the `Checkstyle` plug image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-checkstyle.png[Checkstyle] -Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on the `+` icon in the `Configuration file` section. There, you'll have to define where the checkstyle rules should be picked from. In the image above, we've picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build's GitHub repository (e.g. for the `checkstyle.xml` : `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml`). We need to provide the following variables: +Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on the `+` icon in the `Configuration file` section. There, you'll have to define where the checkstyle rules should be picked from. In the image above, we've picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build's GitHub repository (e.g. for the `checkstyle.xml` : `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/spring-cloud-build-tools/src/main/resources/checkstyle.xml`). We need to provide the following variables: -- `checkstyle.header.file` - please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` URL. -- `checkstyle.suppressions.file` - default suppressions. Please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` URL. +- `checkstyle.header.file` - please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` URL. +- `checkstyle.suppressions.file` - default suppressions. Please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` URL. - `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`. IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources. diff --git a/dep.txt b/dep.txt new file mode 100644 index 000000000..ca45143cb --- /dev/null +++ b/dep.txt @@ -0,0 +1,158 @@ +[INFO] Scanning for projects... +[WARNING] +[WARNING] Some problems were encountered while building the effective model for org.springframework.cloud:spring-cloud-function-context:jar:4.0.0-SNAPSHOT +[WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: com.fasterxml.jackson.core:jackson-databind:jar -> duplicate declaration of version (?) @ line 94, column 15 +[WARNING] +[WARNING] Some problems were encountered while building the effective model for org.springframework.cloud:spring-cloud-function-adapter-aws:jar:4.0.0-SNAPSHOT +[WARNING] 'dependencies.dependency.(groupId:artifactId:type:classifier)' must be unique: org.springframework.boot:spring-boot-starter-test:jar -> duplicate declaration of version (?) @ line 118, column 15 +[WARNING] +[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build. +[WARNING] +[WARNING] For this reason, future Maven versions might no longer support building such malformed projects. +[WARNING] +[INFO] +[INFO] ------< org.springframework.cloud:spring-cloud-function-context >------- +[INFO] Building spring-cloud-function-context 4.0.0-SNAPSHOT +[INFO] --------------------------------[ jar ]--------------------------------- +[INFO] +[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ spring-cloud-function-context --- +[INFO] org.springframework.cloud:spring-cloud-function-context:jar:4.0.0-SNAPSHOT +[INFO] +- net.jodah:typetools:jar:0.6.2:compile +[INFO] +- org.springframework.boot:spring-boot-autoconfigure:jar:3.0.0-SNAPSHOT:compile +[INFO] | \- org.springframework.boot:spring-boot:jar:3.0.0-SNAPSHOT:compile +[INFO] | \- org.springframework:spring-context:jar:6.0.0-SNAPSHOT:compile +[INFO] | +- org.springframework:spring-aop:jar:6.0.0-SNAPSHOT:compile +[INFO] | \- org.springframework:spring-expression:jar:6.0.0-SNAPSHOT:compile +[INFO] +- org.springframework.cloud:spring-cloud-function-core:jar:4.0.0-SNAPSHOT:compile +[INFO] | +- io.projectreactor:reactor-core:jar:3.4.17:compile +[INFO] | | \- org.reactivestreams:reactive-streams:jar:1.0.3:compile +[INFO] | \- org.springframework:spring-core:jar:6.0.0-SNAPSHOT:compile +[INFO] | \- org.springframework:spring-jcl:jar:6.0.0-SNAPSHOT:compile +[INFO] +- org.springframework:spring-messaging:jar:6.0.0-SNAPSHOT:compile +[INFO] | \- org.springframework:spring-beans:jar:6.0.0-SNAPSHOT:compile +[INFO] +- org.springframework:spring-web:jar:6.0.0-SNAPSHOT:compile +[INFO] +- com.fasterxml.jackson.module:jackson-module-kotlin:jar:2.13.2:compile +[INFO] | \- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.2:compile +[INFO] +- javax.annotation:javax.annotation-api:jar:1.3.2:compile +[INFO] +- javax.activation:javax.activation-api:jar:1.2.0:compile +[INFO] +- org.springframework.boot:spring-boot-configuration-processor:jar:3.0.0-SNAPSHOT:compile +[INFO] +- com.google.code.gson:gson:jar:2.9.0:compile +[INFO] +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.2.1:compile +[INFO] | \- com.fasterxml.jackson.core:jackson-core:jar:2.13.2:compile +[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:3.0.0-SNAPSHOT:compile +[INFO] | +- org.springframework.boot:spring-boot-starter:jar:3.0.0-SNAPSHOT:compile +[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:3.0.0-SNAPSHOT:compile +[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.11:compile +[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.11:compile +[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile +[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile +[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile +[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:2.0.0:compile +[INFO] | | \- org.yaml:snakeyaml:jar:1.30:compile +[INFO] | +- org.springframework.boot:spring-boot-test:jar:3.0.0-SNAPSHOT:compile +[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:3.0.0-SNAPSHOT:compile +[INFO] | +- com.jayway.jsonpath:json-path:jar:2.7.0:compile +[INFO] | | \- net.minidev:json-smart:jar:2.4.8:compile +[INFO] | | \- net.minidev:accessors-smart:jar:2.4.8:compile +[INFO] | | \- org.ow2.asm:asm:jar:9.1:compile +[INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:3.0.1:compile +[INFO] | | \- com.sun.activation:jakarta.activation:jar:2.0.1:compile +[INFO] | +- org.assertj:assertj-core:jar:3.22.0:compile +[INFO] | +- org.hamcrest:hamcrest:jar:2.2:compile +[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.8.2:compile +[INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.8.2:compile +[INFO] | | | +- org.opentest4j:opentest4j:jar:1.2.0:compile +[INFO] | | | +- org.junit.platform:junit-platform-commons:jar:1.8.2:compile +[INFO] | | | \- org.apiguardian:apiguardian-api:jar:1.1.2:compile +[INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.8.2:compile +[INFO] | | \- org.junit.jupiter:junit-jupiter-engine:jar:5.8.2:runtime +[INFO] | | \- org.junit.platform:junit-platform-engine:jar:1.8.2:runtime +[INFO] | +- org.mockito:mockito-core:jar:4.5.0:compile +[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.12.9:compile +[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.12.9:compile +[INFO] | | \- org.objenesis:objenesis:jar:3.2:runtime +[INFO] | +- org.mockito:mockito-junit-jupiter:jar:4.5.0:compile +[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:compile +[INFO] | +- org.springframework:spring-test:jar:6.0.0-SNAPSHOT:compile +[INFO] | \- org.xmlunit:xmlunit-core:jar:2.9.0:compile +[INFO] +- io.projectreactor:reactor-test:jar:3.4.17:test +[INFO] +- org.jetbrains.kotlin:kotlin-stdlib-jdk8:jar:1.6.21:compile +[INFO] | +- org.jetbrains.kotlin:kotlin-stdlib:jar:1.6.21:compile +[INFO] | | +- org.jetbrains.kotlin:kotlin-stdlib-common:jar:1.6.21:compile +[INFO] | | \- org.jetbrains:annotations:jar:13.0:compile +[INFO] | \- org.jetbrains.kotlin:kotlin-stdlib-jdk7:jar:1.6.21:compile +[INFO] +- org.jetbrains.kotlin:kotlin-reflect:jar:1.6.21:compile +[INFO] +- org.jetbrains.kotlinx:kotlinx-coroutines-reactor:jar:1.6.1:compile +[INFO] | +- org.jetbrains.kotlinx:kotlinx-coroutines-reactive:jar:1.6.1:compile +[INFO] | \- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:jar:1.6.1:compile +[INFO] +- org.apache.avro:avro:jar:1.10.2:compile +[INFO] | +- org.apache.commons:commons-compress:jar:1.20:compile +[INFO] | \- org.slf4j:slf4j-api:jar:1.7.36:compile +[INFO] +- io.cloudevents:cloudevents-spring:jar:2.2.0:compile +[INFO] | \- io.cloudevents:cloudevents-core:jar:2.2.0:compile +[INFO] | \- io.cloudevents:cloudevents-api:jar:2.2.0:compile +[INFO] +- org.springframework.boot:spring-boot-starter-actuator:jar:3.0.0-SNAPSHOT:compile +[INFO] | \- org.springframework.boot:spring-boot-actuator-autoconfigure:jar:3.0.0-SNAPSHOT:compile +[INFO] | +- org.springframework.boot:spring-boot-actuator:jar:3.0.0-SNAPSHOT:compile +[INFO] | \- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.2:runtime +[INFO] +- io.micrometer:micrometer-observation:jar:2.0.0-SNAPSHOT:compile +[INFO] | \- io.micrometer:micrometer-commons:jar:2.0.0-SNAPSHOT:compile +[INFO] +- io.micrometer:micrometer-core:jar:2.0.0-SNAPSHOT:compile +[INFO] | +- org.hdrhistogram:HdrHistogram:jar:2.1.12:compile +[INFO] | \- org.latencyutils:LatencyUtils:jar:2.0.3:runtime +[INFO] +- io.micrometer:micrometer-tracing-api:jar:1.0.0-SNAPSHOT:compile +[INFO] | \- aopalliance:aopalliance:jar:1.0:compile +[INFO] \- io.micrometer:micrometer-tracing-integration-test:jar:1.0.0-SNAPSHOT:test +[INFO] +- io.micrometer:micrometer-tracing-test:jar:1.0.0-SNAPSHOT:test +[INFO] +- io.micrometer:micrometer-tracing-reporter-wavefront:jar:1.0.0-SNAPSHOT:test +[INFO] +- io.micrometer:micrometer-test:jar:2.0.0-SNAPSHOT:test +[INFO] | +- ru.lanwen.wiremock:wiremock-junit5:jar:1.3.1:test +[INFO] | \- com.github.tomakehurst:wiremock-jre8-standalone:jar:2.33.1:test +[INFO] +- io.micrometer:micrometer-tracing-bridge-brave:jar:1.0.0-SNAPSHOT:test +[INFO] +- io.zipkin.brave:brave:jar:5.13.2:test +[INFO] +- io.zipkin.brave:brave-context-slf4j:jar:5.13.2:test +[INFO] +- io.zipkin.brave:brave-instrumentation-http:jar:5.13.2:test +[INFO] +- io.zipkin.brave:brave-tests:jar:5.13.2:test +[INFO] | \- junit:junit:jar:4.13.2:test +[INFO] | \- org.hamcrest:hamcrest-core:jar:2.2:test +[INFO] +- io.zipkin.aws:brave-propagation-aws:jar:0.23.2:test +[INFO] +- io.zipkin.reporter2:zipkin-reporter-brave:jar:2.16.1:test +[INFO] +- io.micrometer:micrometer-tracing-bridge-otel:jar:1.0.0-SNAPSHOT:test +[INFO] | +- io.opentelemetry:opentelemetry-api:jar:1.13.0:test +[INFO] | | \- io.opentelemetry:opentelemetry-context:jar:1.13.0:test +[INFO] | +- io.opentelemetry:opentelemetry-extension-aws:jar:1.13.0:test +[INFO] | +- io.opentelemetry:opentelemetry-semconv:jar:1.13.0-alpha:test +[INFO] | +- io.opentelemetry:opentelemetry-sdk-common:jar:1.13.0:test +[INFO] | \- io.opentelemetry:opentelemetry-sdk:jar:1.13.0:test +[INFO] | +- io.opentelemetry:opentelemetry-sdk-metrics:jar:1.13.0-alpha:test +[INFO] | \- io.opentelemetry:opentelemetry-sdk-logs:jar:1.13.0-alpha:test +[INFO] +- io.opentelemetry:opentelemetry-exporter-zipkin:jar:1.13.0:test +[INFO] | \- io.zipkin.reporter2:zipkin-sender-okhttp3:jar:2.16.1:test +[INFO] | \- com.squareup.okhttp3:okhttp:jar:4.9.3:test +[INFO] | \- com.squareup.okio:okio:jar:2.8.0:test +[INFO] +- io.opentelemetry:opentelemetry-sdk-trace:jar:1.13.0:test +[INFO] +- io.opentelemetry:opentelemetry-extension-trace-propagators:jar:1.13.0:test +[INFO] +- io.opentelemetry.instrumentation:opentelemetry-instrumentation-api:jar:1.12.1-alpha:test +[INFO] +- io.zipkin.zipkin2:zipkin:jar:2.23.0:test +[INFO] +- io.zipkin.reporter2:zipkin-reporter:jar:2.16.1:test +[INFO] +- io.zipkin.reporter2:zipkin-sender-urlconnection:jar:2.16.1:test +[INFO] +- io.zipkin.reporter2:zipkin-sender-kafka:jar:2.16.1:test +[INFO] +- io.zipkin.reporter2:zipkin-sender-activemq-client:jar:2.16.1:test +[INFO] +- io.zipkin.reporter2:zipkin-sender-amqp-client:jar:2.16.1:test +[INFO] +- com.wavefront:wavefront-sdk-java:jar:3.0.0:test +[INFO] | +- com.google.code.findbugs:jsr305:jar:3.0.2:test +[INFO] | +- com.tdunning:t-digest:jar:3.2:test +[INFO] | \- com.google.guava:guava:jar:31.0.1-jre:test +[INFO] | +- com.google.guava:failureaccess:jar:1.0.1:test +[INFO] | +- com.google.guava:listenablefuture:jar:9999.0-empty-to-avoid-conflict-with-guava:test +[INFO] | +- org.checkerframework:checker-qual:jar:3.12.0:test +[INFO] | +- com.google.errorprone:error_prone_annotations:jar:2.7.1:test +[INFO] | \- com.google.j2objc:j2objc-annotations:jar:1.3:test +[INFO] \- com.wavefront:wavefront-internal-reporter-java:jar:1.7.10:test +[INFO] \- com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:jar:2.13.2:test +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 1.157 s +[INFO] Finished at: 2022-05-03T19:59:07+02:00 +[INFO] ------------------------------------------------------------------------ diff --git a/docs/pom.xml b/docs/pom.xml index 8ebe1c228..91cd38422 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -7,7 +7,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT pom Spring Cloud Function Docs @@ -17,6 +17,8 @@ ${basedir}/.. 3.4 deploy + + none @@ -42,6 +44,12 @@ org.asciidoctor asciidoctor-maven-plugin + + + ${project.version} + ${spring-boot.version} + + diff --git a/docs/src/main/asciidoc/README.adoc b/docs/src/main/asciidoc/README.adoc index 6d0f4cf8d..f5eb5c158 100644 --- a/docs/src/main/asciidoc/README.adoc +++ b/docs/src/main/asciidoc/README.adoc @@ -12,8 +12,8 @@ include::getting-started.adoc[] == Building -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/building.adoc[] +include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/docs/src/main/asciidoc/building.adoc[] == Contributing -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[] +include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/docs/src/main/asciidoc/contributing.adoc[] diff --git a/docs/src/main/asciidoc/_intro.adoc b/docs/src/main/asciidoc/_intro.adoc index 3812b572c..c1895c454 100644 --- a/docs/src/main/asciidoc/_intro.adoc +++ b/docs/src/main/asciidoc/_intro.adoc @@ -49,5 +49,4 @@ endpoints and/or message stream listeners/publishers with RabbitMQ, Kafka etc. * _Packaging functions for deployments, specific to the target platform (e.g., Project Riff, AWS Lambda and more)_ * _Adapters to expose function to the outside world as HTTP endpoints etc._ * _Deploying a JAR file containing such an application context with an isolated classloader, so that you can pack them together in a single JVM._ -* _Compiling strings which are Java function bodies into bytecode, and then turning them into `@Beans` that can be wrapped as above._ * _Adapters for https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-aws[AWS Lambda], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[Azure], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp[Google Cloud Functions], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk[Apache OpenWhisk] and possibly other "serverless" service providers._ diff --git a/docs/src/main/asciidoc/adapters/aws-intro.adoc b/docs/src/main/asciidoc/adapters/aws-intro.adoc index bc4295cdf..d8c68efe5 100644 --- a/docs/src/main/asciidoc/adapters/aws-intro.adoc +++ b/docs/src/main/asciidoc/adapters/aws-intro.adoc @@ -90,6 +90,9 @@ Also, note that since AWS does not allow dots `.` and/or hyphens`-` in the name dots with underscores and hyphens with camel case. So for example `spring.cloud.function.definition` becomes `spring_cloud_function_definition` and `spring.cloud.function.routing-expression` becomes `spring_cloud_function_routingExpression`. +===== AWS Function Routing with Custom Runtime + +When using <> Function Routing works the same way. All you need is to specify `functionRouter` as AWS Handler the same way you would use the name of the function as handler. ==== Notes on JAR Layout @@ -116,24 +119,35 @@ then additional transformers must be configured as part of the maven-shade-plugi org.springframework.boot spring-boot-maven-plugin + 2.7.1 - - false - true - aws - - - META-INF/spring.handlers - - - META-INF/spring.factories - - - META-INF/spring.schemas - - - + + + + shade + + + false + true + aws + + + META-INF/spring.handlers + + + META-INF/spring.factories + + + META-INF/spring.schemas + + + META-INF/spring.components + + + + + ---- diff --git a/docs/src/main/asciidoc/adapters/aws.adoc b/docs/src/main/asciidoc/adapters/aws.adoc index 39d4bc9f2..0912ded38 100644 --- a/docs/src/main/asciidoc/adapters/aws.adoc +++ b/docs/src/main/asciidoc/adapters/aws.adoc @@ -31,12 +31,19 @@ public class FuncApplication implements ApplicationContextInitializer new FunctionRegistration>(function()) - .type(FunctionType.from(Foo.class).to(Bar.class).getType())); + .type(FunctionTypeUtils.functionType(Foo.class, Bar.class))); } } ``` +== AWS Context + +In a typical implementation of AWS Handler user has access to AWS _context_ object. With function approach you can have the same experience if you need it. +Upon each invocation the framework will add `aws-context` message header containing the AWS _context_ instance for that particular invocation. So if you need to access it +you can simply have `Message` as an input parameter to your function and then access `aws-context` from message headers. +For convenience we provide AWSLambdaUtils.AWS_CONTEXT constant. + == Platform Specific Features === HTTP and API Gateway diff --git a/docs/src/main/asciidoc/functional.adoc b/docs/src/main/asciidoc/functional.adoc index 0bbe7e1c1..a12f520ac 100644 --- a/docs/src/main/asciidoc/functional.adoc +++ b/docs/src/main/asciidoc/functional.adoc @@ -40,7 +40,7 @@ public class DemoApplication implements ApplicationContextInitializer new FunctionRegistration<>(uppercase()) - .type(FunctionType.from(String.class).to(String.class))); + .type(FunctionTypeUtils.functionType(String.class, String.class))); } } @@ -265,19 +265,19 @@ like the `@Autowired` `TestRestTemplate`, are standard Spring Boot features. And to help with correct dependencies here is the excerpt from POM -[source, xml] +[source, xml, subs=attributes+] ---- org.springframework.boot spring-boot-starter-parent - 2.2.2.RELEASE + {spring-boot-version} . . . . org.springframework.cloud spring-cloud-function-web - 3.0.1.BUILD-SNAPSHOT + {project-version} org.springframework.boot @@ -292,12 +292,6 @@ And to help with correct dependencies here is the excerpt from POM org.springframework.boot spring-boot-starter-test test - - - org.junit.vintage - junit-vintage-engine - - ---- @@ -305,7 +299,6 @@ Or you could write a test for a non-HTTP app using just the `FunctionCatalog`. F [source, java] ---- -@RunWith(SpringRunner.class) @FunctionalSpringBootTest public class FunctionalTests { @@ -313,7 +306,7 @@ public class FunctionalTests { private FunctionCatalog catalog; @Test - public void words() throws Exception { + public void words() { Function function = catalog.lookup(Function.class, "uppercase"); assertThat(function.apply("hello")).isEqualTo("HELLO"); diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc index 4b37510bd..ff8153e03 100644 --- a/docs/src/main/asciidoc/index.adoc +++ b/docs/src/main/asciidoc/index.adoc @@ -1,5 +1,5 @@ = Spring Cloud Function Reference Documentation -Mark Fisher, Dave Syer, Oleg Zhurakousky, Anshul Mehra +Mark Fisher, Dave Syer, Oleg Zhurakousky, Anshul Mehra, Dan Dobrin, Chris Bono *{project-version}* diff --git a/docs/src/main/asciidoc/sagan-index.adoc b/docs/src/main/asciidoc/sagan-index.adoc index 5e4faf7e1..71e9b2113 100644 --- a/docs/src/main/asciidoc/sagan-index.adoc +++ b/docs/src/main/asciidoc/sagan-index.adoc @@ -18,7 +18,6 @@ Spring Cloud Function features: * _Packaging functions for deployments, specific to the target platform (e.g., Project Riff, AWS Lambda and more)_ * _Adapters to expose function to the outside world as HTTP endpoints etc._ * _Deploying a JAR file containing such an application context with an isolated classloader, so that you can pack them together in a single JVM._ -* _Compiling strings which are Java function bodies into bytecode, and then turning them into `@Beans` that can be wrapped as above._ * _Adapters for https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-aws[AWS Lambda], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-azure[Microsoft Azure], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp[Google Cloud Functions], https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk[Apache OpenWhisk] and possibly other "serverless" service providers._ Here's a complete, executable, testable Spring Boot application (implementing a simple string manipulation): diff --git a/docs/src/main/asciidoc/spring-cloud-function.adoc b/docs/src/main/asciidoc/spring-cloud-function.adoc index 12d7784f5..a297c9475 100644 --- a/docs/src/main/asciidoc/spring-cloud-function.adoc +++ b/docs/src/main/asciidoc/spring-cloud-function.adoc @@ -16,7 +16,7 @@ Mark Fisher, Dave Syer, Oleg Zhurakousky, Anshul Mehra, Dan Dobrin include::_intro.adoc[] -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing-docs.adoc[] +include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/3.1.x/docs/src/main/asciidoc/contributing-docs.adoc[] == Getting Started @@ -50,6 +50,23 @@ and available to us since Java 8. - Function - Consumer +In a nutshell, any bean in your Application Context that is of type `Supplier`, `Function` or `Consumer` could be registered with `FunctionCatalog`. +This means that it could benefit from all the features described in this reference manual. + +==== Filtering ineligible functions +A typical Application Context may include beans that are valid java functions, but not intended to be candidates to be registered with `FunctionCatalog`. +Such beans could be auto-configurations from other projects or any other beans that qualify to be Java functions. +The framework provides default filtering of known beans that should not be candidates for registration with function catalog. +You can also add to this list additional beans by providing coma delimited list of bean definition names using +`spring.cloud.function.ineligible-definitions` property + +For example, + +[source, test] +---- +spring.cloud.function.ineligible-definitions=foo,bar +---- + ==== Supplier Supplier can be _reactive_ - `Supplier>` or _imperative_ - `Supplier`. From the invocation standpoint this should make no difference @@ -145,7 +162,7 @@ public class RoutingFunction implements Function { The routing instructions could be communicated in several ways. We support providing instructions via Message headers, System properties as well as pluggable strategy. So let's look at some of the details -*MessageRoutingCallback* +==== MessageRoutingCallback The `MessageRoutingCallback` is a strategy to assist with determining the name of the route-to function definition. @@ -191,16 +208,20 @@ downstream. So effectively you letting the framework to benefit from the work yo If the input argument is of type `Message`, you can communicate routing instruction by setting one of `spring.cloud.function.definition` or `spring.cloud.function.routing-expression` Message headers. +As the name of the property suggests `spring.cloud.function.routing-expression` relies on Spring Expression Language (SpEL). For more static cases you can use `spring.cloud.function.definition` header which allows you to provide the name of a single function (e.g., `...definition=foo`) or a composition instruction (e.g., `...definition=foo|bar|baz`). -For more dynamic cases you can use `spring.cloud.function.routing-expression` header which allows -you to use Spring Expression Language (SpEL) and provide SpEL expression that should resolve +For more dynamic cases you can use `spring.cloud.function.routing-expression` header and provide SpEL expression that should resolve into definition of a function (as described above). NOTE: SpEL evaluation context's root object is the actual input argument, so in the case of `Message` you can construct expression that has access to both `payload` and `headers` (e.g., `spring.cloud.function.routing-expression=headers.function_name`). +IMPORTANT: SpEL allows user to provide string representation of Java code to be executed. Given that the `spring.cloud.function.routing-expression` could be provided via Message headers means that ability to set such expression could be exposed to the end user (i.e., HTTP Headers when using web module) which could result in some problems (e.g., malicious code). To manage that, all expressions coming via Message headers will only be evaluated against `SimpleEvaluationContext` which has limited functionality and designed to only evaluate the context object (Message in our case). On the other hand, all expressions that are set via property or system variable are evaluated against `StandardEvaluationContext`, which allows for full flexibility of Java language. +While setting expression via system/application property or environment variable is generally considered to be secure as it is not exposed to the end user in normal cases, there are cases where visibility as well as capability to update system, application and environment variables are indeed exposed to the end user via Spring Boot Actuator endpoints provided either by some of the Spring projects or third parties or custom implementation by the end user. Such endpoints must be secured using industry standard web security practices. +Spring Cloud Function does not expose any of such endpoints. + In specific execution environments/models the adapters are responsible to translate and communicate `spring.cloud.function.definition` and/or `spring.cloud.function.routing-expression` via Message header. For example, when using _spring-cloud-function-web_ you can provide `spring.cloud.function.definition` as an HTTP @@ -230,8 +251,29 @@ conflict resolutions in the event multiple mechanisms are used at the same time, 2. Message Headers (If function is imperative and no `MessageRoutingCallback` provided) 3. Application Properties (Any function) +*Unroutable Messages* + +In the event route-to function is not available in catalog you will get an exception stating that. + +There are cases when such behavior is not desired and you may want to have some "catch-all" type function which can handle such messages. +To accomplish that, framework provides `org.springframework.cloud.function.context.DefaultMessageRoutingHandler` strategy. All you need to do is register it as a bean. +Its default implementation will simply log the fact that the message is un-routable, but will allow message flow to proceed without the exception, effectively dropping the un-routable message. +If you want something more sophisticated all you need to do is provide your own implementation of this strategy and register it as a bean. + +[source, java] +---- +@Bean +public DefaultMessageRoutingHandler defaultRoutingHandler() { + return new DefaultMessageRoutingHandler() { + @Override + public void accept(Message message) { + // do something really cool + } + }; +} +---- -*Function Filtering* +==== Function Filtering Filtering is the type of routing where there are only two paths - 'go' or 'discard'. In terms of functions it mean you only want to invoke a certain function if some condition returns 'true', otherwise you want to discard input. However, when it comes to discarding input there are many interpretation of what it could mean in the context of your application. @@ -261,6 +303,58 @@ due to the nature of the reactive functions which are invoked only once to pass is handled by the reactor, hence we can not access and/or rely on the routing instructions communicated via individual values (e.g., Message). +==== Multiple Routers + +By default the framework will always have a single routing function configured as described in previous sections. However, there are times when you may need more then one routing function. +In that case you can create your own instance of the `RoutingFunction` bean in addition to the existing one as long as you give it a name other than `functionRouter`. + +You can pass `spring.cloud.function.routing-expression` or `spring.cloud.function.definition` to RoutinFunction as key/value pairs in the map. + +Here is a simple example + +---- +@Configuration +protected static class MultipleRouterConfiguration { + + @Bean + RoutingFunction mySpecialRouter(FunctionCatalog functionCatalog, BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) { + Map propertiesMap = new HashMap<>(); + propertiesMap.put(FunctionProperties.PREFIX + ".routing-expression", "'reverse'"); + return new RoutingFunction(functionCatalog, propertiesMap, new BeanFactoryResolver(beanFactory), routingCallback); + } + + @Bean + public Function reverse() { + return v -> new StringBuilder(v).reverse().toString(); + } + + @Bean + public Function uppercase() { + return String::toUpperCase; + } +} +---- + +and a test that demonstrates how it works + +` +---- +@Test +public void testMultipleRouters() { + System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "'uppercase'"); + FunctionCatalog functionCatalog = this.configureCatalog(MultipleRouterConfiguration.class); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello").build(); + assertThat(function.apply(message)).isEqualTo("HELLO"); + + function = functionCatalog.lookup("mySpecialRouter"); + assertThat(function).isNotNull(); + message = MessageBuilder.withPayload("hello").build(); + assertThat(function.apply(message)).isEqualTo("olleh"); +} +---- + === Input/Output Enrichment There are often times when you need to modify or refine an incoming or outgoing Message and to keep your code clean of non-functional concerns. You don’t want to do it inside of your business logic. @@ -376,6 +470,41 @@ IMPORTANT: IMPORTANT: At the moment, function arity is *only* supported for reac where evaluation and computation on confluence of events typically requires view into a stream of events rather than single event. +=== Input Header propagation + +In a typical scenario input Message headers are not propagated to output and rightfully so, since the output of a function may be an input to something else requiring it's own set of Message headers. +However, there are times when such propagation may be necessary so Spring Cloud Function provides several mechanisms to accomplish this. + +First you can always copy headers manually. For example, if you have a Function with the signature that takes `Message` and returns `Message` (i.e., `Function`), you can simply and selectively copy headers yourselves. Remember, if your function returns Message, the framework will not do anything to it other then properly converting its payload. +However, such approach may prove to be a bit tedious, especially in cases when you simply want to copy all headers. +To assist with cases like this we provide a simple property that would allow you to set a boolean flag on a function where you want input headers to be propagated. +The property is `copy-input-headers`. + +For example, let's assume you have the following configuration: + +[source, java] +---- +@EnableAutoConfiguration +@Configuration +protected static class InputHeaderPropagationConfiguration { + + @Bean + public Function uppercase() { + return x -> x.toUpperCase(); + } +} +---- + +As you know you can still invoke this function by sending a Message to it (framework will take care of type conversion and payload extraction) + +By simply setting `spring.cloud.function.configuration.uppercase.copy-input-headers` to `true`, the following assertion will be true as well + +---- +Function, Message> uppercase = catalog.lookup("uppercase", "application/json"); +Message result = uppercase.apply(MessageBuilder.withPayload("bob").setHeader("foo", "bar").build()); +assertThat(result.getHeaders()).containsKey("foo"); +---- + === Type conversion (Content-Type negotiation) Content-Type negotiation is one of the core features of Spring Cloud Function as it allows to not only transform the incoming data to the types declared @@ -587,17 +716,23 @@ plain text and JSON. |=== -As the table above shows the behaviour of the endpoint depends on the method and also the type of incoming request data. When the incoming data -is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or `Flux`), then the response -will also contain a single value. +As the table above shows the behavior of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or `Flux`), then the response will also contain a single value. For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream". -Functions and consumers that are declared with input and output in `Message` will see the request headers on the input messages, and the output message headers will be converted to HTTP headers. +Functions and consumers that are declared with input and output in `Message` will see the request headers as _message headers_, and the output _message headers_ will be converted to HTTP headers. +The _payload_ of the Message will be a `body` or empty string if there is no `body` or it is null. When POSTing text the response format might be different with Spring Boot 2.0 and older versions, depending on the content negotiation (provide content type and accept headers for the best results). See <> to see the details and example on how to test such application. +==== HTTP Request Parameters +As you have noticed from the previous table, you can pass an argument to a function as path variable (i.e., `/{function}/{item}`). +For example, `http://localhost:8080/uppercase/foo` will result in calling `uppercase` function with its input parameter being `foo`. + +While this is the recommended approach and the one that fits most use cases cases, there are times when you have to deal with HTTP request parameters (e.g., `http://localhost:8080/uppercase/foo?name=Bill`) +The framework will treat HTTP request parameters similar to the HTTP headers by storing them in the `Message` headers under the header key `http_request_param` +with its value being a `Map` of request parameters, so in order to access them your function input signature should accept `Message` type (e.g., `Function, String>`). For convenience we provide `HeaderUtils.HTTP_REQUEST_PARAM` constant. === Function Mapping rules @@ -832,98 +967,6 @@ NOTE: This particular deployment option may or may not have Spring Cloud Functio include::functional.adoc[leveloffset=+1] -== Dynamic Compilation - -There is a sample app that uses the function compiler to create a -function from a configuration property. The vanilla "function-sample" -also has that feature. And there are some scripts that you can run to -see the compilation happening at run time. To run these examples, -change into the `scripts` directory: - ----- -cd scripts ----- - -Also, start a RabbitMQ server locally (e.g. execute `rabbitmq-server`). - -Start the Function Registry Service: - ----- -./function-registry.sh ----- - -Register a Function: - ----- -./registerFunction.sh -n uppercase -f "f->f.map(s->s.toString().toUpperCase())" ----- - -Run a REST Microservice using that Function: - ----- -./web.sh -f uppercase -p 9000 -curl -H "Content-Type: text/plain" -H "Accept: text/plain" localhost:9000/uppercase -d foo ----- - -Register a Supplier: - ----- -./registerSupplier.sh -n words -f "()->Flux.just(\"foo\",\"bar\")" ----- - -Run a REST Microservice using that Supplier: - ----- -./web.sh -s words -p 9001 -curl -H "Accept: application/json" localhost:9001/words ----- - -Register a Consumer: - ----- -./registerConsumer.sh -n print -t String -f "System.out::println" ----- - -Run a REST Microservice using that Consumer: - ----- -./web.sh -c print -p 9002 -curl -X POST -H "Content-Type: text/plain" -d foo localhost:9002/print ----- - -Run Stream Processing Microservices: - -First register a streaming words supplier: - ----- -./registerSupplier.sh -n wordstream -f "()->Flux.interval(Duration.ofMillis(1000)).map(i->\"message-\"+i)" ----- - -Then start the source (supplier), processor (function), and sink (consumer) apps -(in reverse order): - ----- -./stream.sh -p 9103 -i uppercaseWords -c print -./stream.sh -p 9102 -i words -f uppercase -o uppercaseWords -./stream.sh -p 9101 -s wordstream -o words ----- - -The output will appear in the console of the sink app (one message per second, converted to uppercase): - ----- -MESSAGE-0 -MESSAGE-1 -MESSAGE-2 -MESSAGE-3 -MESSAGE-4 -MESSAGE-5 -MESSAGE-6 -MESSAGE-7 -MESSAGE-8 -MESSAGE-9 -... ----- - == Serverless Platform Adapters As well as being able to run as a standalone process, a Spring Cloud diff --git a/pom.xml b/pom.xml index ce54fa4ca..a89461181 100644 --- a/pom.xml +++ b/pom.xml @@ -6,20 +6,17 @@ spring-cloud-function-parent Spring Cloud Function Parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT pom org.springframework.cloud spring-cloud-build - 3.1.1-SNAPSHOT + 3.1.10-SNAPSHOT - 1.8 - ${java.version} - ${java.version} 1.0.27.RELEASE spring-cloud-function true @@ -127,20 +124,13 @@ - + + + javax.annotation + javax.annotation-api + + - - java11+ - - [11,) - - - - javax.annotation - javax.annotation-api - - - core @@ -162,11 +152,11 @@ spring-cloud-function-web spring-cloud-starter-function-web spring-cloud-starter-function-webflux - spring-cloud-function-samples + spring-cloud-function-deployer spring-cloud-function-adapters spring-cloud-function-rsocket - spring-cloud-function-kotlin + docs diff --git a/scripts/function-registry.sh b/scripts/function-registry.sh deleted file mode 100755 index 2655daf11..000000000 --- a/scripts/function-registry.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -java -jar ../spring-cloud-function-compiler/target/spring-cloud-function-compiler-2.0.0.BUILD-SNAPSHOT.jar diff --git a/scripts/registerConsumer.sh b/scripts/registerConsumer.sh deleted file mode 100755 index 0cfbf985b..000000000 --- a/scripts/registerConsumer.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -while getopts ":n:f:t:" opt; do - case $opt in - n) - NAME=$OPTARG - ;; - f) - FUNC=$OPTARG - ;; - t) - TYPE=$OPTARG - ;; - esac -done - -curl -X POST -H "Content-Type: text/plain" -d $FUNC localhost:8080/consumer/$NAME?type=$TYPE diff --git a/scripts/registerFunction.sh b/scripts/registerFunction.sh deleted file mode 100755 index 5638f4db8..000000000 --- a/scripts/registerFunction.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -while getopts ":n:f:i:o:" opt; do - case $opt in - n) - NAME=$OPTARG - ;; - f) - FUNC=$OPTARG - ;; - i) - INTYPE=$OPTARG - ;; - o) - OUTTYPE=$OPTARG - ;; - esac -done - -curl -X POST -H "Content-Type: text/plain" -d $FUNC "localhost:8080/function/$NAME?inputType=$INTYPE&outputType=$OUTTYPE" - diff --git a/scripts/registerSupplier.sh b/scripts/registerSupplier.sh deleted file mode 100755 index df853198f..000000000 --- a/scripts/registerSupplier.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -while getopts ":n:f:t:" opt; do - case $opt in - n) - NAME=$OPTARG - ;; - f) - FUNC=$OPTARG - ;; - t) - TYPE=$OPTARG - ;; - esac -done - -curl -X POST -H "Content-Type: text/plain" -d $FUNC localhost:8080/supplier/$NAME?type=$TYPE diff --git a/scripts/stream.sh b/scripts/stream.sh deleted file mode 100755 index 0d22c78e4..000000000 --- a/scripts/stream.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -PREFIX="--spring.cloud.function.import" -DIR="file:///tmp/function-registry" - -tokenize() { - local IFS=, - local TOKENS=($1) - echo ${TOKENS[@]} -} - -DURATION=0 - -while getopts ":i:s:f:c:o:p:d:" opt; do - case $opt in - i) - IN=--spring.cloud.stream.bindings.input.destination=$OPTARG - ;; - s) - FUNC=$OPTARG - TYPE="$PREFIX.$FUNC.type=supplier" - RESOURCE="$PREFIX.$FUNC.location=$DIR/suppliers/$FUNC.fun" - ;; - f) - FUNC=$OPTARG - for i in `tokenize $OPTARG`; do - RESOURCE="$RESOURCE $PREFIX.${i}.location=$DIR/functions/${i}.fun" - TYPE="$TYPE $PREFIX.${i}.type=function" - done - ;; - c) - FUNC=$OPTARG - TYPE="$PREFIX.$FUNC.type=consumer" - RESOURCE="$PREFIX.$FUNC.location=$DIR/consumers/$FUNC.fun" - ;; - o) - OUT=--spring.cloud.stream.bindings.output.destination=$OPTARG - ;; - p) - PORT=$OPTARG - ;; - d) - DURATION=$OPTARG - ;; - esac -done - -java -jar ../spring-cloud-function-samples/function-sample-compiler/target/function-sample-compiler-2.0.0.BUILD-SNAPSHOT.jar\ - --management.security.enabled=false\ - --server.port=$PORT\ - --spring.cloud.function.stream.endpoint=$FUNC\ - --spring.cloud.function.stream.interval=$DURATION\ - $IN\ - $OUT\ - $RESOURCE\ - $TYPE diff --git a/scripts/task.sh b/scripts/task.sh deleted file mode 100755 index cc804d1e2..000000000 --- a/scripts/task.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -while getopts ":s:f:c:" opt; do - case $opt in - s) - SUPP=$OPTARG - ;; - f) - FUNC=$OPTARG - ;; - c) - CONS=$OPTARG - ;; - esac -done - -java -noverify -XX:TieredStopAtLevel=1 -Xss256K -Xms16M -Xmx256M -XX:MaxMetaspaceSize=128M -jar ../spring-cloud-function-task/target/spring-cloud-function-task-2.0.0.BUILD-SNAPSHOT.jar\ - --lambda.supplier=$SUPP --lambda.function=$FUNC --lambda.consumer=$CONS diff --git a/scripts/web.sh b/scripts/web.sh deleted file mode 100755 index 58b3d065c..000000000 --- a/scripts/web.sh +++ /dev/null @@ -1,27 +0,0 @@ -#!/bin/bash - -while getopts ":s:f:c:p:" opt; do - case $opt in - s) - FUNC=$OPTARG - TYPE=supplier - ;; - f) - FUNC=$OPTARG - TYPE=function - ;; - c) - FUNC=$OPTARG - TYPE=consumer - ;; - p) - PORT=$OPTARG - ;; - esac -done - -java -jar ../spring-cloud-function-samples/function-sample-compiler/target/function-sample-compiler-2.0.0.BUILD-SNAPSHOT.jar\ - --spring.cloud.function.import.$FUNC.type=$TYPE\ - --spring.cloud.function.import.$FUNC.location=file:///tmp/function-registry/$TYPE's'/$FUNC.fun\ - --management.security.enabled=false\ - --server.port=$PORT diff --git a/spring-cloud-function-adapters/pom.xml b/spring-cloud-function-adapters/pom.xml index 092a20be6..cfbd18d20 100644 --- a/spring-cloud-function-adapters/pom.xml +++ b/spring-cloud-function-adapters/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT spring-cloud-function-adapter-parent diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml index df68a3f43..e1fcf5e4f 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml @@ -13,7 +13,7 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT @@ -35,10 +35,6 @@ org.springframework.boot spring-boot-starter - - com.fasterxml.jackson.datatype - jackson-datatype-joda - com.amazonaws aws-lambda-java-log4j diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSCompanionAutoConfiguration.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSCompanionAutoConfiguration.java index 59e10f589..2d61cf37c 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSCompanionAutoConfiguration.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSCompanionAutoConfiguration.java @@ -16,10 +16,13 @@ package org.springframework.cloud.function.adapter.aws; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.springframework.cloud.function.json.JacksonMapper; import org.springframework.cloud.function.json.JsonMapper; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.messaging.converter.MessageConverter; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.util.CollectionUtils; /** * @@ -27,11 +30,18 @@ * @since 3.2 * */ -@Configuration(proxyBeanMethods = false) -public class AWSCompanionAutoConfiguration { +public class AWSCompanionAutoConfiguration implements ApplicationContextInitializer { - @Bean - public MessageConverter awsTypesConverter(JsonMapper jsonMapper) { - return new AWSTypesMessageConverter(jsonMapper); + @Override + public void initialize(GenericApplicationContext applicationContext) { + applicationContext.registerBean("awsTypesMessageConverter", AWSTypesMessageConverter.class, + () -> { + if (CollectionUtils.isEmpty(applicationContext.getBeansOfType(JsonMapper.class).values())) { + return new AWSTypesMessageConverter(new JacksonMapper(new ObjectMapper())); + } + else { + return new AWSTypesMessageConverter(applicationContext.getBean(JsonMapper.class)); + } + }); } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSLambdaUtils.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSLambdaUtils.java index 72fb9181c..16282a9e0 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSLambdaUtils.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSLambdaUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2021 the original author or authors. + * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package org.springframework.cloud.function.adapter.aws; -import java.io.ByteArrayInputStream; -import java.lang.reflect.ParameterizedType; +import java.io.IOException; +import java.io.InputStream; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -25,181 +25,165 @@ import java.util.concurrent.atomic.AtomicReference; import com.amazonaws.services.lambda.runtime.Context; -import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; -import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; -import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; -import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.http.HttpStatus; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.StreamUtils; /** * * @author Oleg Zhurakousky * */ -final class AWSLambdaUtils { +public final class AWSLambdaUtils { private static Log logger = LogFactory.getLog(AWSLambdaUtils.class); static final String AWS_API_GATEWAY = "aws-api-gateway"; - private AWSLambdaUtils() { + static final String AWS_EVENT = "aws-event"; - } + static final String IS_BASE64_ENCODED = "isBase64Encoded"; + + static final String STATUS_CODE = "statusCode"; + + static final String BODY = "body"; + + static final String HEADERS = "headers"; + + /** + * The name of the headers that stores AWS Context object. + */ + public static final String AWS_CONTEXT = "aws-context"; + + private AWSLambdaUtils() { - public static Message generateMessage(byte[] payload, MessageHeaders headers, - Type inputType, JsonMapper objectMapper) { - return generateMessage(payload, headers, inputType, objectMapper, null); } static boolean isSupportedAWSType(Type inputType) { + if (FunctionTypeUtils.isMessage(inputType) || FunctionTypeUtils.isPublisher(inputType)) { + inputType = FunctionTypeUtils.getImmediateGenericType(inputType, 0); + } String typeName = inputType.getTypeName(); return typeName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent") || typeName.equals("com.amazonaws.services.lambda.runtime.events.S3Event") || typeName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent") || typeName.equals("com.amazonaws.services.lambda.runtime.events.SNSEvent") || typeName.equals("com.amazonaws.services.lambda.runtime.events.SQSEvent") + || typeName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayCustomAuthorizerEvent") || typeName.equals("com.amazonaws.services.lambda.runtime.events.KinesisEvent"); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static Message generateMessage(byte[] payload, MessageHeaders headers, - Type inputType, JsonMapper objectMapper, @Nullable Context awsContext) { - - if (logger.isInfoEnabled()) { - logger.info("Incoming JSON Event: " + new String(payload)); - } - - if (FunctionTypeUtils.isMessage(inputType)) { + @SuppressWarnings("rawtypes") + public static Message generateMessage(InputStream payload, Type inputType, boolean isSupplier, JsonMapper jsonMapper, Context context) throws IOException { + if (inputType != null && FunctionTypeUtils.isMessage(inputType)) { inputType = FunctionTypeUtils.getImmediateGenericType(inputType, 0); } - - MessageBuilder messageBuilder = null; - if (inputType != null && isSupportedAWSType(inputType)) { - PojoSerializer serializer = LambdaEventSerializers.serializerFor(FunctionTypeUtils.getRawType(inputType), Thread.currentThread().getContextClassLoader()); - Object event = serializer.fromJson(new ByteArrayInputStream(payload)); - messageBuilder = MessageBuilder.withPayload(event); - if (event instanceof APIGatewayProxyRequestEvent || event instanceof APIGatewayV2HTTPEvent) { - messageBuilder.setHeader(AWS_API_GATEWAY, true); - logger.info("Incoming request is API Gateway"); + if (inputType != null && InputStream.class.isAssignableFrom(FunctionTypeUtils.getRawType(inputType))) { + MessageBuilder msgBuilder = MessageBuilder.withPayload(payload); + if (context != null) { + msgBuilder.setHeader(AWSLambdaUtils.AWS_CONTEXT, context); } + return msgBuilder.build(); } else { - Object request; - try { - request = objectMapper.fromJson(payload, Object.class); - } - catch (Exception e) { - throw new IllegalStateException(e); - } + return generateMessage(StreamUtils.copyToByteArray(payload), inputType, isSupplier, jsonMapper, context); + } + } - if (request instanceof Map) { - logger.info("Incoming MAP: " + request); - if (((Map) request).containsKey("httpMethod")) { //API Gateway - logger.info("Incoming request is API Gateway"); - boolean mapInputType = (inputType instanceof ParameterizedType && ((Class) ((ParameterizedType) inputType).getRawType()).isAssignableFrom(Map.class)); - if (mapInputType) { - messageBuilder = MessageBuilder.withPayload(request).setHeader("httpMethod", ((Map) request).get("httpMethod")); - messageBuilder.setHeader(AWS_API_GATEWAY, true); - } - else { - messageBuilder = createMessageBuilderForPOJOFunction(objectMapper, (Map) request); - } - } - else if ((((Map) request).containsKey("routeKey") && ((Map) request).containsKey("version"))) { - logger.info("Incoming request is API Gateway v2.0"); - messageBuilder = createMessageBuilderForPOJOFunction(objectMapper, (Map) request); - } - Object providedHeaders = ((Map) request).remove("headers"); - if (providedHeaders != null && providedHeaders instanceof Map) { - messageBuilder.removeHeader("headers"); - messageBuilder.copyHeaders((Map) providedHeaders); - } - } - else if (request instanceof Iterable) { - messageBuilder = MessageBuilder.withPayload(request); - } + public static Message generateMessage(byte[] payload, Type inputType, boolean isSupplier, JsonMapper jsonMapper) { + return generateMessage(payload, inputType, isSupplier, jsonMapper, null); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Message generateMessage(byte[] payload, Type inputType, boolean isSupplier, JsonMapper jsonMapper, Context context) { + if (logger.isInfoEnabled()) { + logger.info("Received: " + new String(payload, StandardCharsets.UTF_8)); } - if (messageBuilder == null) { - messageBuilder = MessageBuilder.withPayload(payload); + Object structMessage = jsonMapper.fromJson(payload, Object.class); + boolean isApiGateway = structMessage instanceof Map + && (((Map) structMessage).containsKey("httpMethod") || + (((Map) structMessage).containsKey("routeKey") && ((Map) structMessage).containsKey("version"))); + + Message requestMessage; + MessageBuilder builder = MessageBuilder.withPayload(payload); + if (isApiGateway) { + builder.setHeader(AWSLambdaUtils.AWS_API_GATEWAY, true); } - if (awsContext != null) { - messageBuilder.setHeader("aws-context", awsContext); + if (!isSupplier && AWSLambdaUtils.isSupportedAWSType(inputType)) { + builder.setHeader(AWSLambdaUtils.AWS_EVENT, true); } - logger.info("Incoming request headers: " + headers); - - return messageBuilder.copyHeaders(headers).build(); + if (context != null) { + builder.setHeader(AWSLambdaUtils.AWS_CONTEXT, context); + } + // + if (structMessage instanceof Map && ((Map) structMessage).containsKey("headers")) { + builder.copyHeaders((Map) ((Map) structMessage).get("headers")); + } + requestMessage = builder.build(); + return requestMessage; } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private static MessageBuilder createMessageBuilderForPOJOFunction(JsonMapper objectMapper, Map request) { - Object body = request.remove("body"); - try { - body = body instanceof String - ? String.valueOf(body).getBytes(StandardCharsets.UTF_8) - : objectMapper.toJson(body); + private static byte[] extractPayload(Message msg, JsonMapper objectMapper) { + if (msg.getPayload() instanceof byte[]) { + return (byte[]) msg.getPayload(); } - catch (Exception e) { - throw new IllegalStateException(e); + else { + return objectMapper.toJson(msg.getPayload()); } - logger.info("Body is " + body); - - MessageBuilder messageBuilder = MessageBuilder.withPayload(body).copyHeaders(request); - messageBuilder.setHeader(AWS_API_GATEWAY, true); - return messageBuilder; } @SuppressWarnings({ "rawtypes", "unchecked" }) - public static byte[] generateOutput(Message requestMessage, Message responseMessage, + public static byte[] generateOutput(Message requestMessage, Message responseMessage, JsonMapper objectMapper, Type functionOutputType) { Class outputClass = FunctionTypeUtils.getRawType(functionOutputType); if (outputClass != null) { String outputClassName = outputClass.getName(); if (outputClassName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse") || - outputClassName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent")) { - return responseMessage.getPayload(); + outputClassName.equals("com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent") || + outputClassName.equals("com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent") || + outputClassName.equals("com.amazonaws.services.lambda.runtime.events.IamPolicyResponse")) { + return extractPayload((Message) responseMessage, objectMapper); } } - byte[] responseBytes = responseMessage == null ? "\"OK\"".getBytes() : responseMessage.getPayload(); + byte[] responseBytes = responseMessage == null ? "\"OK\"".getBytes() : extractPayload((Message) responseMessage, objectMapper); if (requestMessage.getHeaders().containsKey(AWS_API_GATEWAY) && ((boolean) requestMessage.getHeaders().get(AWS_API_GATEWAY))) { Map response = new HashMap(); - response.put("isBase64Encoded", false); + response.put(IS_BASE64_ENCODED, responseMessage != null && responseMessage.getHeaders().containsKey(IS_BASE64_ENCODED) + ? responseMessage.getHeaders().get(IS_BASE64_ENCODED) : false); AtomicReference headers = new AtomicReference<>(); int statusCode = HttpStatus.OK.value(); if (responseMessage != null) { headers.set(responseMessage.getHeaders()); - statusCode = headers.get().containsKey("statusCode") - ? (int) headers.get().get("statusCode") + statusCode = headers.get().containsKey(STATUS_CODE) + ? (int) headers.get().get(STATUS_CODE) : HttpStatus.OK.value(); } - response.put("statusCode", statusCode); + response.put(STATUS_CODE, statusCode); if (isRequestKinesis(requestMessage)) { HttpStatus httpStatus = HttpStatus.valueOf(statusCode); response.put("statusDescription", httpStatus.toString()); } String body = responseMessage == null - ? "\"OK\"" : new String(responseMessage.getPayload(), StandardCharsets.UTF_8).replaceAll("\\\"", ""); - response.put("body", body); - + ? "\"OK\"" : new String(extractPayload((Message) responseMessage, objectMapper), StandardCharsets.UTF_8); + response.put(BODY, body); if (responseMessage != null) { Map responseHeaders = new HashMap<>(); headers.get().keySet().forEach(key -> responseHeaders.put(key, headers.get().get(key).toString())); - response.put("headers", responseHeaders); + response.put(HEADERS, responseHeaders); } try { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java index 76daaf19a..5d3e88bc6 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.adapter.aws; import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; import java.util.Map; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; @@ -55,8 +56,11 @@ class AWSTypesMessageConverter extends JsonMessageConverter { @Override protected boolean canConvertFrom(Message message, @Nullable Class targetClass) { - if (message.getHeaders().containsKey(AWSLambdaUtils.AWS_API_GATEWAY) && ((boolean) message.getHeaders().get(AWSLambdaUtils.AWS_API_GATEWAY))) { - return true; + if (message.getHeaders().containsKey(AWSLambdaUtils.AWS_API_GATEWAY)) { + return ((boolean) message.getHeaders().get(AWSLambdaUtils.AWS_API_GATEWAY)); + } + if (message.getHeaders().containsKey(AWSLambdaUtils.AWS_EVENT)) { + return ((boolean) message.getHeaders().get(AWSLambdaUtils.AWS_EVENT)); } return false; } @@ -97,6 +101,9 @@ protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) @Override protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { + if (payload instanceof String && headers.containsKey(AWSLambdaUtils.IS_BASE64_ENCODED) && (boolean) headers.get(AWSLambdaUtils.IS_BASE64_ENCODED)) { + return ((String) payload).getBytes(StandardCharsets.UTF_8); + } return jsonMapper.toJson(payload); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java index 83d9f256d..ce35d1508 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2021 the original author or authors. + * Copyright 2021-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,13 @@ package org.springframework.cloud.function.adapter.aws; +import java.io.PrintWriter; +import java.io.StringWriter; import java.net.SocketException; import java.net.URI; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -30,6 +30,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.json.JsonMapper; @@ -41,10 +42,12 @@ import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.client.RestTemplate; + + import static org.apache.http.HttpHeaders.USER_AGENT; /** @@ -61,6 +64,7 @@ public final class CustomRuntimeEventLoop implements SmartLifecycle { private static Log logger = LogFactory.getLog(CustomRuntimeEventLoop.class); static final String LAMBDA_VERSION_DATE = "2018-06-01"; + private static final String LAMBDA_ERROR_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/error"; private static final String LAMBDA_RUNTIME_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/next"; private static final String LAMBDA_INVOCATION_URL_TEMPLATE = "http://{0}/{1}/runtime/invocation/{2}/response"; private static final String USER_AGENT_VALUE = String.format( @@ -130,33 +134,74 @@ private void eventLoop(ConfigurableApplicationContext context) { } if (response != null) { - FunctionInvocationWrapper function = locateFunction(environment, functionCatalog, response.getHeaders().getContentType()); - Message eventMessage = AWSLambdaUtils.generateMessage(response.getBody().getBytes(StandardCharsets.UTF_8), - fromHttp(response.getHeaders()), function.getInputType(), mapper); - if (logger.isDebugEnabled()) { - logger.debug("Event message: " + eventMessage); - } - String requestId = response.getHeaders().getFirst("Lambda-Runtime-Aws-Request-Id"); - String invocationUrl = MessageFormat - .format(LAMBDA_INVOCATION_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId); + try { + FunctionInvocationWrapper function = locateFunction(environment, functionCatalog, response.getHeaders()); + + Message eventMessage = AWSLambdaUtils + .generateMessage(response.getBody().getBytes(StandardCharsets.UTF_8), function.getInputType(), function.isSupplier(), mapper); + + if (logger.isDebugEnabled()) { + logger.debug("Event message: " + eventMessage); + } + + String invocationUrl = MessageFormat + .format(LAMBDA_INVOCATION_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId); + + String traceId = response.getHeaders().getFirst("Lambda-Runtime-Trace-Id"); + if (StringUtils.hasText(traceId)) { + if (logger.isDebugEnabled()) { + logger.debug("Lambda-Runtime-Trace-Id: " + traceId); + } + System.setProperty("com.amazonaws.xray.traceHeader", traceId); + } - Message responseMessage = (Message) function.apply(eventMessage); + Message responseMessage = (Message) function.apply(eventMessage); - if (responseMessage != null && logger.isDebugEnabled()) { - logger.debug("Reply from function: " + responseMessage); + if (responseMessage != null && logger.isDebugEnabled()) { + logger.debug("Reply from function: " + responseMessage); + } + + byte[] outputBody = AWSLambdaUtils.generateOutput(eventMessage, responseMessage, mapper, function.getOutputType()); + ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(invocationUrl)) + .header(USER_AGENT, USER_AGENT_VALUE) + .body(outputBody), Object.class); + + if (logger.isInfoEnabled()) { + logger.info("Result POST status: " + result.getStatusCode()); + } + } + catch (Exception e) { + this.propagateAwsError(requestId, e, mapper, runtimeApi, rest); } + } + } + } - byte[] outputBody = AWSLambdaUtils.generateOutput(eventMessage, responseMessage, mapper, function.getOutputType()); - ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(invocationUrl)) + private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String runtimeApi, RestTemplate rest) { + String errorMessage = e.getMessage(); + String errorType = e.getClass().getSimpleName(); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + e.printStackTrace(pw); + String stackTrace = sw.toString(); + Map em = new HashMap<>(); + em.put("errorMessage", errorMessage); + em.put("errorType", errorType); + em.put("stackTrace", stackTrace); + byte[] outputBody = mapper.toJson(em); + try { + String errorUrl = MessageFormat.format(LAMBDA_ERROR_URL_TEMPLATE, runtimeApi, LAMBDA_VERSION_DATE, requestId); + ResponseEntity result = rest.exchange(RequestEntity.post(URI.create(errorUrl)) .header(USER_AGENT, USER_AGENT_VALUE) .body(outputBody), Object.class); - - if (logger.isInfoEnabled()) { - logger.info("Result POST status: " + result.getStatusCode()); - } + if (logger.isInfoEnabled()) { + logger.info("Result ERROR status: " + result.getStatusCode()); } } + catch (Exception e2) { + throw new IllegalArgumentException("Failed to report error", e2); + } } private ResponseEntity pollForData(RestTemplate rest, RequestEntity requestEntity) { @@ -172,7 +217,8 @@ private ResponseEntity pollForData(RestTemplate rest, RequestEntity map = new LinkedHashMap<>(); - for (String name : headers.keySet()) { - Collection values = multi(headers.get(name)); - name = name.toLowerCase(); - Object value = values == null ? null - : (values.size() == 1 ? values.iterator().next() : values); - if (name.toLowerCase().equals(HttpHeaders.CONTENT_TYPE.toLowerCase())) { - name = MessageHeaders.CONTENT_TYPE; + private static String extractVersion() { + try { + String path = CustomRuntimeEventLoop.class.getProtectionDomain().getCodeSource().getLocation().toString(); + int endIndex = path.lastIndexOf('.'); + if (endIndex < 0) { + return "UNKNOWN-VERSION"; } - map.put(name, value); + int startIndex = path.lastIndexOf("/") + 1; + return path.substring(startIndex, endIndex).replace("spring-cloud-function-adapter-aws-", ""); } - return new MessageHeaders(map); - } - - private Collection multi(Object value) { - return value instanceof Collection ? (Collection) value : Arrays.asList(value); - } - - private static String extractVersion() { - String path = CustomRuntimeEventLoop.class.getProtectionDomain().getCodeSource().getLocation().toString(); - int endIndex = path.lastIndexOf('.'); - if (endIndex < 0) { + catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to detect version", e); + } return "UNKNOWN-VERSION"; } - int startIndex = path.lastIndexOf("/") + 1; - return path.substring(startIndex, endIndex).replace("spring-cloud-function-adapter-aws-", ""); } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeInitializer.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeInitializer.java index 60e8a48f8..174183d83 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeInitializer.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeInitializer.java @@ -20,8 +20,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.cloud.function.context.AbstractSpringFunctionAdapterInitializer; -import org.springframework.cloud.function.context.config.ContextFunctionCatalogInitializer; -import org.springframework.cloud.function.web.source.DestinationResolver; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.SmartLifecycle; import org.springframework.context.support.GenericApplicationContext; @@ -49,12 +47,6 @@ public void initialize(GenericApplicationContext context) { SmartLifecycle.class, () -> new CustomRuntimeEventLoop(context)); } } - else if (ContextFunctionCatalogInitializer.enabled - && context.getEnvironment().getProperty("spring.functional.enabled", Boolean.class, false)) { - if (context.getBeanFactory().getBeanNamesForType(DestinationResolver.class, false, false).length == 0) { - context.registerBean(LambdaDestinationResolver.class, () -> new LambdaDestinationResolver()); - } - } } private boolean isCustomRuntime(Environment environment) { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java index 071a2c6a4..54ea419aa 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,23 +19,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; import java.util.List; -import java.util.Map; import java.util.Set; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestStreamHandler; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.datatype.joda.JodaModule; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; @@ -43,6 +33,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionalSpringApplication; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.context.config.RoutingFunction; @@ -53,10 +44,8 @@ import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.Environment; import org.springframework.messaging.Message; -import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; @@ -76,39 +65,22 @@ public class FunctionInvoker implements RequestStreamHandler { private FunctionInvocationWrapper function; - public FunctionInvoker() { + private volatile String functionDefinition; + + public FunctionInvoker(String functionDefinition) { + this.functionDefinition = functionDefinition; this.start(); } - @SuppressWarnings({ "rawtypes", "unchecked" }) + public FunctionInvoker() { + this(null); + } + + @SuppressWarnings({ "rawtypes" }) @Override public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { - final byte[] payload = StreamUtils.copyToByteArray(input); - - if (logger.isInfoEnabled()) { - logger.info("Received: " + new String(payload, StandardCharsets.UTF_8)); - } - - Object structMessage = this.jsonMapper.fromJson(payload, Object.class); - - boolean isApiGateway = structMessage instanceof Map - && (((Map) structMessage).containsKey("httpMethod") || - (((Map) structMessage).containsKey("routeKey") && ((Map) structMessage).containsKey("version"))); - - - // TODO we should eventually completely delegate to message converter - Message requestMessage; - if (isApiGateway) { - MessageBuilder builder = MessageBuilder.withPayload(payload).setHeader(AWSLambdaUtils.AWS_API_GATEWAY, true); - if (structMessage instanceof Map && ((Map) structMessage).containsKey("headers")) { - builder.copyHeaders((Map) ((Map) structMessage).get("headers")); - } - requestMessage = builder.build(); - } - else { - requestMessage = AWSLambdaUtils - .generateMessage(payload, new MessageHeaders(Collections.emptyMap()), function.getInputType(), this.jsonMapper, context); - } + Message requestMessage = AWSLambdaUtils + .generateMessage(input, this.function.getInputType(), this.function.isSupplier(), jsonMapper, context); Object response = this.function.apply(requestMessage); byte[] responseBytes = this.buildResult(requestMessage, response); @@ -118,28 +90,31 @@ public void handleRequest(InputStream input, OutputStream output, Context contex @SuppressWarnings("unchecked") private byte[] buildResult(Message requestMessage, Object output) throws IOException { - Message responseMessage; + Message responseMessage = null; if (output instanceof Publisher) { List result = new ArrayList<>(); for (Object value : Flux.from((Publisher) output).toIterable()) { - if (logger.isInfoEnabled()) { - logger.info("Response value: " + value); + if (logger.isDebugEnabled()) { + logger.debug("Response value: " + value); } result.add(value); } if (result.size() > 1) { output = result; } - else { + else if (result.size() == 1) { output = result.get(0); } - - if (logger.isInfoEnabled()) { - logger.info("OUTPUT: " + output + " - " + output.getClass().getName()); + else { + output = null; + } + if (output != null) { + if (logger.isDebugEnabled()) { + logger.debug("OUTPUT: " + output + " - " + output.getClass().getName()); + } + byte[] payload = this.jsonMapper.toJson(output); + responseMessage = MessageBuilder.withPayload(payload).build(); } - - byte[] payload = this.jsonMapper.toJson(output); - responseMessage = MessageBuilder.withPayload(payload).build(); } else { responseMessage = (Message) output; @@ -155,39 +130,33 @@ private void start() { : SpringApplication.run(new Class[] {startClass, AWSCompanionAutoConfiguration.class}, properties); Environment environment = context.getEnvironment(); - String functionName = environment.getProperty("spring.cloud.function.definition"); + if (!StringUtils.hasText(this.functionDefinition)) { + this.functionDefinition = environment.getProperty(FunctionProperties.FUNCTION_DEFINITION); + } + FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class); this.jsonMapper = context.getBean(JsonMapper.class); if (this.jsonMapper instanceof JacksonMapper) { ((JacksonMapper) this.jsonMapper).configureObjectMapper(objectMapper -> { if (!objectMapper.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)) { - SimpleModule module = new SimpleModule(); - module.addDeserializer(Date.class, new JsonDeserializer() { - @Override - public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) - throws IOException { - Calendar calendar = Calendar.getInstance(); - calendar.setTimeInMillis(jsonParser.getValueAsLong()); - return calendar.getTime(); - } - }); - objectMapper.registerModule(module); - objectMapper.registerModule(new JodaModule()); objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); } }); } if (logger.isInfoEnabled()) { - logger.info("Locating function: '" + functionName + "'"); + logger.info("Locating function: '" + this.functionDefinition + "'"); } - this.function = functionCatalog.lookup(functionName, "application/json"); - - Set names = functionCatalog.getNames(null); - if (this.function == null && !CollectionUtils.isEmpty(names)) { + this.function = functionCatalog.lookup(this.functionDefinition, "application/json"); + if (this.function == null) { if (logger.isInfoEnabled()) { + if (!StringUtils.hasText(this.functionDefinition)) { + logger.info("Failed to determine default function. Please use 'spring.cloud.function.definition' property " + + "or pass function definition as a constructir argument to this FunctionInvoker"); + } + Set names = functionCatalog.getNames(null); if (names.size() == 1) { logger.info("Will default to RoutingFunction, since it is the only function available in FunctionCatalog." + "Expecting 'spring.cloud.function.definition' or 'spring.cloud.function.routing-expression' as Message headers. " @@ -206,14 +175,11 @@ public Date deserialize(JsonParser jsonParser, DeserializationContext deserializ if (this.function.isOutputTypePublisher()) { this.function.setSkipOutputConversion(true); } - Assert.notNull(this.function, "Failed to lookup function " + functionName); - - if (!StringUtils.hasText(functionName)) { - functionName = this.function.getFunctionDefinition(); - } + Assert.notNull(this.function, "Failed to lookup function " + this.functionDefinition); + this.functionDefinition = this.function.getFunctionDefinition(); if (logger.isInfoEnabled()) { - logger.info("Located function: '" + functionName + "'"); + logger.info("Located function: '" + this.functionDefinition + "'"); } } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java index 186f629c3..be75093b7 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandler.java @@ -33,7 +33,9 @@ /** * @author Dave Syer * @author Oleg Zhurakousky + * @deprecated since 3.2.7 in favor of {@link FunctionInvoker} */ +@Deprecated public class SpringBootStreamHandler extends AbstractSpringFunctionAdapterInitializer implements RequestStreamHandler { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/resources/META-INF/spring.factories b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/resources/META-INF/spring.factories index b10fc836b..d78111519 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/resources/META-INF/spring.factories @@ -1,4 +1,4 @@ org.springframework.context.ApplicationContextInitializer=\ -org.springframework.cloud.function.adapter.aws.CustomRuntimeInitializer +org.springframework.cloud.function.adapter.aws.CustomRuntimeInitializer,org.springframework.cloud.function.adapter.aws.AWSCompanionAutoConfiguration org.springframework.boot.env.EnvironmentPostProcessor=\ org.springframework.cloud.function.adapter.aws.CustomRuntimeEnvironmentPostProcessor \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java index 3b34e8b16..b743a26b0 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java @@ -18,19 +18,25 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.Field; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.APIGatewayCustomAuthorizerEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent; +import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent; import com.amazonaws.services.lambda.runtime.events.KinesisEvent; import com.amazonaws.services.lambda.runtime.events.S3Event; import com.amazonaws.services.lambda.runtime.events.SNSEvent; @@ -38,15 +44,20 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.function.json.JacksonMapper; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.messaging.Message; import org.springframework.messaging.converter.AbstractMessageConverter; +import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.MimeType; +import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.fail; @@ -62,29 +73,32 @@ public class FunctionInvokerTests { String jsonCollection = "[\"Ricky\",\"Julien\",\"Bubbles\"]"; - String sampleLBEvent = "{" + - " \"requestContext\": {" + - " \"elb\": {" + - " \"targetGroupArn\": \"arn:aws:elasticloadbalancing:region:123456789012:targetgroup/my-target-group/6d0ecf831eec9f09\"" + - " }" + - " }," + - " \"httpMethod\": \"GET\"," + - " \"path\": \"/\"," + - " \"headers\": {" + - " \"accept\": \"text/html,application/xhtml+xml\"," + - " \"accept-language\": \"en-US,en;q=0.8\"," + - " \"content-type\": \"text/plain\"," + - " \"cookie\": \"cookies\"," + - " \"host\": \"lambda-846800462-us-east-2.elb.amazonaws.com\"," + - " \"user-agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6)\"," + - " \"x-amzn-trace-id\": \"Root=1-5bdb40ca-556d8b0c50dc66f0511bf520\"," + - " \"x-forwarded-for\": \"72.21.198.66\"," + - " \"x-forwarded-port\": \"443\"," + - " \"x-forwarded-proto\": \"https\"" + - " }," + - " \"isBase64Encoded\": false," + - " \"body\": \"request_body\"" + - "}"; + String sampleLBEvent = "{\n" + + " \"requestContext\": {\n" + + " \"elb\": {\n" + + " \"targetGroupArn\": \"arn:aws:elasticloadbalancing:us-east-1:XXXXXXXXXXX:targetgroup/sample/6d0ecf831eec9f09\"\n" + + " }\n" + + " },\n" + + " \"httpMethod\": \"GET\",\n" + + " \"path\": \"/\",\n" + + " \"queryStringParameters\": {},\n" + + " \"headers\": {\n" + + " \"accept\": \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\",\n" + + " \"accept-encoding\": \"gzip\",\n" + + " \"accept-language\": \"en-US,en;q=0.5\",\n" + + " \"connection\": \"keep-alive\",\n" + + " \"cookie\": \"name=value\",\n" + + " \"host\": \"lambda-YYYYYYYY.elb.amazonaws.com\",\n" + + " \"upgrade-insecure-requests\": \"1\",\n" + + " \"user-agent\": \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:60.0) Gecko/20100101 Firefox/60.0\",\n" + + " \"x-amzn-trace-id\": \"Root=1-5bdb40ca-556d8b0c50dc66f0511bf520\",\n" + + " \"x-forwarded-for\": \"192.0.2.1\",\n" + + " \"x-forwarded-port\": \"80\",\n" + + " \"x-forwarded-proto\": \"http\"\n" + + " },\n" + + " \"body\": \"Hello from ELB\",\n" + + " \"isBase64Encoded\": false\n" + + "}"; String sampleSQSEvent = "{\n" + " \"Records\": [\n" + @@ -442,12 +456,31 @@ public class FunctionInvokerTests { " \"isBase64Encoded\": false\n" + "}"; + String gwAuthorizerEvent = "{\n" + + " \"type\":\"TOKEN\",\n" + + " \"authorizationToken\":\"allow\",\n" + + " \"methodArn\":\"arn:aws:execute-api:us-west-2:123456789012:ymy8tbxw7b/*/GET/\"\n" + + "}"; + @BeforeEach public void before() throws Exception { System.clearProperty("MAIN_CLASS"); System.clearProperty("spring.cloud.function.routing-expression"); System.clearProperty("spring.cloud.function.definition"); - this.getEnvironment().clear(); + //this.getEnvironment().clear(); + } + + @Test + public void testAPIGatewayCustomAuthorizerEvent() throws Exception { + System.setProperty("MAIN_CLASS", AuthorizerConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "acceptAuthorizerEvent"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.gwAuthorizerEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + String result = new String(output.toByteArray(), StandardCharsets.UTF_8); + assertThat(result).contains("APIGatewayCustomAuthorizerEvent(version=null, type=TOKEN"); } @Test @@ -480,11 +513,17 @@ public void testKinesisStringEvent() throws Exception { public void testKinesisEvent() throws Exception { System.setProperty("MAIN_CLASS", KinesisConfiguration.class.getName()); System.setProperty("spring.cloud.function.definition", "inputKinesisEvent"); - FunctionInvoker invoker = new FunctionInvoker(); + FunctionInvoker invoker = new FunctionInvoker() { + @Override + public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + assertThat(context).isNotNull(); + super.handleRequest(input, output, context); + } + }; InputStream targetStream = new ByteArrayInputStream(this.sampleKinesisEvent.getBytes()); ByteArrayOutputStream output = new ByteArrayOutputStream(); - invoker.handleRequest(targetStream, output, null); + invoker.handleRequest(targetStream, output, new TestContext()); String result = new String(output.toByteArray(), StandardCharsets.UTF_8); assertThat(result).contains("49590338271490256608559692538361571095921575989136588898"); @@ -545,6 +584,19 @@ public void testSQSEvent() throws Exception { assertThat(result).contains("arn:aws:sqs:eu-central-1:123456789012:MyQueue"); } + @Test + public void testSQSEventWithConstructorArg() throws Exception { + System.setProperty("MAIN_CLASS", SQSConfiguration.class.getName()); + FunctionInvoker invoker = new FunctionInvoker("inputSQSEvent"); + + InputStream targetStream = new ByteArrayInputStream(this.sampleSQSEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + String result = new String(output.toByteArray(), StandardCharsets.UTF_8); + assertThat(result).contains("arn:aws:sqs:eu-central-1:123456789012:MyQueue"); + } + @Test public void testSQSEventAsMessage() throws Exception { System.setProperty("MAIN_CLASS", SQSConfiguration.class.getName()); @@ -643,12 +695,6 @@ public void testS3StringEvent() throws Exception { @Test public void testS3Event() throws Exception { - -// S3EventSerializer ser = new S3EventSerializer().withClass(S3Event.class).withClassLoader(S3Event.class.getClassLoader()); -// InputStream targetStream = new ByteArrayInputStream(this.s3Event.getBytes()); -// S3Event event = ser.fromJson(targetStream); -// System.out.println(event); - System.setProperty("MAIN_CLASS", S3Configuration.class.getName()); System.setProperty("spring.cloud.function.definition", "inputS3Event"); FunctionInvoker invoker = new FunctionInvoker(); @@ -658,7 +704,7 @@ public void testS3Event() throws Exception { invoker.handleRequest(targetStream, output, null); String result = new String(output.toByteArray(), StandardCharsets.UTF_8); - assertThat(result).contains("s3SchemaVersion"); + assertThat(result).contains("ObjectCreated:Put"); } @Test @@ -672,7 +718,7 @@ public void testS3EventAsMessage() throws Exception { invoker.handleRequest(targetStream, output, null); String result = new String(output.toByteArray(), StandardCharsets.UTF_8); - assertThat(result).contains("s3SchemaVersion"); + assertThat(result).contains("ObjectCreated:Put"); } @Test @@ -689,6 +735,102 @@ public void testS3EventAsMap() throws Exception { assertThat(result).contains("s3SchemaVersion"); } + + @Test + public void testLBEventStringInOut() throws Exception { + System.setProperty("MAIN_CLASS", LBConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "echoString"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.sampleLBEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result.get("body")).isEqualTo("\"Hello from ELB\""); + } + + @Test + public void testS3EventReactive() throws Exception { + System.setProperty("MAIN_CLASS", S3Configuration.class.getName()); + System.setProperty("spring.cloud.function.definition", "echoStringFlux"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.s3Event.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + String result = new String(output.toByteArray(), StandardCharsets.UTF_8); + assertThat(result).contains("s3SchemaVersion"); + } + + @Test + public void testLBEvent() throws Exception { + System.setProperty("MAIN_CLASS", LBConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "inputLBEvent"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.sampleLBEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result.get("body")).isEqualTo("\"Hello from ELB\""); + } + + @Test + public void testLBEventReturningMessage() throws Exception { + System.setProperty("MAIN_CLASS", LBConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "inputOutputLBEventAsMessage"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.sampleLBEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + System.out.println("RESULT: " + result); + assertThat(result.get("body")).isEqualTo("\"Hello\""); + assertThat((boolean) result.get("isBase64Encoded")).isFalse(); + assertThat(((Map) result.get("headers")).get("foo")).isEqualTo("bar"); + assertThat(result.get("statusCode")).isEqualTo(200); + } + + @Test + public void testLBEventAsMessage() throws Exception { + System.setProperty("MAIN_CLASS", LBConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "inputLBEventAsMessage"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.sampleLBEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, Mockito.mock(Context.class)); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result.get("body")).isEqualTo("\"Hello from ELB\""); + } + + @Test + public void testLBEventInOut() throws Exception { + System.setProperty("MAIN_CLASS", LBConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "inputOutputLBEvent"); + FunctionInvoker invoker = new FunctionInvoker() { + @Override + public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + assertThat(context).isNotNull(); + super.handleRequest(input, output, context); + } + }; + + InputStream targetStream = new ByteArrayInputStream(this.sampleLBEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, new TestContext()); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result.get("body")).isEqualTo("Hello from ELB"); + } + + @SuppressWarnings("rawtypes") @Test public void testApiGatewayStringEventBody() throws Exception { @@ -701,7 +843,7 @@ public void testApiGatewayStringEventBody() throws Exception { invoker.handleRequest(targetStream, output, null); Map result = mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("HELLO"); + assertThat(result.get("body")).isEqualTo("\"HELLO\""); System.clearProperty("spring.cloud.function.definition"); System.setProperty("spring.cloud.function.routing-expression", "'uppercase'"); @@ -711,7 +853,23 @@ public void testApiGatewayStringEventBody() throws Exception { invoker.handleRequest(targetStream, output, null); result = this.mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("HELLO"); + assertThat(result.get("body")).isEqualTo("\"HELLO\""); + } + + @SuppressWarnings("rawtypes") + @Test + public void testApiGatewayPojoReturninPojo() throws Exception { + System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "uppercasePojoReturnPojo"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEventWithStructuredBody.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map response = mapper.readValue(output.toByteArray(), Map.class); + Person person = mapper.readValue((String) response.get("body"), Person.class); + assertThat(person.getName()).isEqualTo("JIM LAHEY"); } @SuppressWarnings("rawtypes") @@ -726,7 +884,7 @@ public void testApiGatewayPojoEventBody() throws Exception { invoker.handleRequest(targetStream, output, null); Map result = mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("JIM LAHEY"); + assertThat(result.get("body")).isEqualTo("\"JIM LAHEY\""); System.clearProperty("spring.cloud.function.definition"); System.setProperty("spring.cloud.function.routing-expression", "'uppercasePojo'"); @@ -736,7 +894,7 @@ public void testApiGatewayPojoEventBody() throws Exception { invoker.handleRequest(targetStream, output, null); result = this.mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("JIM LAHEY"); + assertThat(result.get("body")).isEqualTo("\"JIM LAHEY\""); } @SuppressWarnings("rawtypes") @@ -744,15 +902,21 @@ public void testApiGatewayPojoEventBody() throws Exception { public void testApiGatewayEvent() throws Exception { System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName()); System.setProperty("spring.cloud.function.definition", "inputApiEvent"); - FunctionInvoker invoker = new FunctionInvoker(); + FunctionInvoker invoker = new FunctionInvoker() { + @Override + public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException { + assertThat(context).isNotNull(); + super.handleRequest(input, output, context); + } + }; InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); ByteArrayOutputStream output = new ByteArrayOutputStream(); - invoker.handleRequest(targetStream, output, null); + invoker.handleRequest(targetStream, output, new TestContext()); Map result = mapper.readValue(output.toByteArray(), Map.class); System.out.println(result); - assertThat(result.get("body")).isEqualTo("hello"); + assertThat(result.get("body")).isEqualTo("\"hello\""); System.clearProperty("spring.cloud.function.definition"); System.setProperty("spring.cloud.function.routing-expression", "'inputApiEvent'"); @@ -762,7 +926,7 @@ public void testApiGatewayEvent() throws Exception { invoker.handleRequest(targetStream, output, null); result = this.mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("hello"); + assertThat(result.get("body")).isEqualTo("\"hello\""); } @SuppressWarnings("rawtypes") @@ -778,7 +942,7 @@ public void testApiGatewayV2Event() throws Exception { Map result = mapper.readValue(output.toByteArray(), Map.class); System.out.println(result); - assertThat(result.get("body")).isEqualTo("Hello from Lambda"); + assertThat(result.get("body")).isEqualTo("\"Hello from Lambda\""); System.clearProperty("spring.cloud.function.definition"); System.setProperty("spring.cloud.function.routing-expression", "'inputApiV2Event'"); @@ -788,7 +952,26 @@ public void testApiGatewayV2Event() throws Exception { invoker.handleRequest(targetStream, output, null); result = this.mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("Hello from Lambda"); + assertThat(result.get("body")).isEqualTo("\"Hello from Lambda\""); + } + + @Test + public void testResponseBase64Encoded() throws Exception { + System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "echoStringMessage"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + JsonMapper mapper = new JacksonMapper(new ObjectMapper()); + + String result = new String(output.toByteArray(), StandardCharsets.UTF_8); + Map resultMap = mapper.fromJson(result, Map.class); + assertThat((boolean) resultMap.get(AWSLambdaUtils.IS_BASE64_ENCODED)).isTrue(); + String body = new String(Base64.getDecoder().decode((String) resultMap.get(AWSLambdaUtils.BODY)), StandardCharsets.UTF_8); + assertThat(body).isEqualTo("hello"); } @SuppressWarnings("rawtypes") @@ -804,7 +987,41 @@ public void testApiGatewayAsSupplier() throws Exception { Map result = mapper.readValue(output.toByteArray(), Map.class); System.out.println(result); - assertThat(result.get("body")).isEqualTo("boom"); + assertThat(result.get("body")).isEqualTo("\"boom\""); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testApiGatewayInAndOutInputStream() throws Exception { + System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "echoInputStreamToString"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result.get("body")).isEqualTo("hello"); + Map headers = (Map) result.get("headers"); + assertThat(headers).isNotEmpty(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testApiGatewayInAndOutInputStreamMsg() throws Exception { + System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "echoInputStreamMsgToString"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result.get("body")).isEqualTo("hello"); + Map headers = (Map) result.get("headers"); + assertThat(headers).isNotEmpty(); } @SuppressWarnings("rawtypes") @@ -841,26 +1058,6 @@ public void testApiGatewayInAndOutV2() throws Exception { assertThat(headers.get("foo")).isEqualTo("bar"); } -// @SuppressWarnings("rawtypes") -// @Test -// public void testApiGatewayInAndOutWithException() throws Exception { -// System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName()); -// System.setProperty("spring.cloud.function.definition", "inputOutputApiEventException"); -// FunctionInvoker invoker = new FunctionInvoker(); -// -// InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); -// ByteArrayOutputStream output = new ByteArrayOutputStream(); -// invoker.handleRequest(targetStream, output, null); -// -// Map result = mapper.readValue(output.toByteArray(), Map.class); -// assertThat(result.get("body")).isEqualTo("Intentional"); -// -// Map headers = (Map) result.get("headers"); -// assertThat(headers.get("foo")).isEqualTo("bar"); -// } - - - @SuppressWarnings("rawtypes") @Test public void testApiGatewayEventAsMessage() throws Exception { @@ -874,7 +1071,7 @@ public void testApiGatewayEventAsMessage() throws Exception { Map result = mapper.readValue(output.toByteArray(), Map.class); System.out.println(result); - assertThat(result.get("body")).isEqualTo("hello"); + assertThat(result.get("body")).isEqualTo("\"hello\""); } @SuppressWarnings("rawtypes") @@ -890,7 +1087,7 @@ public void testApiGatewayEventAsMap() throws Exception { Map result = mapper.readValue(output.toByteArray(), Map.class); System.out.println(result); - assertThat(result.get("body")).isEqualTo("hello"); + assertThat(result.get("body")).isEqualTo("\"hello\""); } @SuppressWarnings("rawtypes") @@ -908,6 +1105,21 @@ public void testApiGatewayEventConsumer() throws Exception { assertThat(result.get("body")).isEqualTo("\"OK\""); } + @SuppressWarnings("rawtypes") + @Test + public void testApiGatewayWithMonoVoidAsReturn() throws Exception { + System.setProperty("MAIN_CLASS", ApiGatewayConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "reactiveWithVoidReturn"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result.get("body")).isEqualTo("\"OK\""); + } + @Test public void testWithDefaultRoutingFailure() throws Exception { System.setProperty("MAIN_CLASS", SampleConfiguration.class.getName()); @@ -937,31 +1149,16 @@ public void testWithDefaultRouting() throws Exception { invoker.handleRequest(targetStream, output, null); Map result = mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("olleh"); + assertThat(result.get("body")).isEqualTo("\"olleh\""); } - @SuppressWarnings("rawtypes") - @Test - public void testWithDefinitionEnvVariable() throws Exception { - - System.setProperty("MAIN_CLASS", SampleConfiguration.class.getName()); - this.getEnvironment().put("SPRING_CLOUD_FUNCTION_DEFINITION", "reverse|uppercase"); - FunctionInvoker invoker = new FunctionInvoker(); - - InputStream targetStream = new ByteArrayInputStream(this.apiGatewayEvent.getBytes()); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - invoker.handleRequest(targetStream, output, null); - - Map result = mapper.readValue(output.toByteArray(), Map.class); - assertThat(result.get("body")).isEqualTo("OLLEH"); - } - - @SuppressWarnings("unchecked") - private Map getEnvironment() throws Exception { - Map env = System.getenv(); - Field field = env.getClass().getDeclaredField("m"); - field.setAccessible(true); - return (Map) field.get(env); + @EnableAutoConfiguration + @Configuration + public static class AuthorizerConfiguration { + @Bean + public Function acceptAuthorizerEvent() { + return v -> v.toString(); + } } @EnableAutoConfiguration @@ -1127,24 +1324,80 @@ public Function echoString() { return v -> v; } + @Bean + public Function, Flux> echoStringFlux() { + return v -> v; + } + @Bean public Function inputS3Event(JsonMapper jsonMapper) { return v -> { System.out.println("Received: " + v); - return jsonMapper.toString(v); + return v.getRecords().get(0).getEventName(); }; } @Bean public Function, String> inputS3EventAsMessage(JsonMapper jsonMapper) { + return m -> { + System.out.println("Received: " + m); + return m.getPayload().getRecords().get(0).getEventName(); + }; + } + + @Bean + public Function, String> inputS3EventAsMap() { return v -> { System.out.println("Received: " + v); - return jsonMapper.toString(v); + return v.toString(); }; } + } + @EnableAutoConfiguration + @Configuration + public static class LBConfiguration { @Bean - public Function, String> inputS3EventAsMap() { + public Function echoString() { + return v -> v; + } + + @Bean + public Function inputLBEvent() { + return v -> { + System.out.println("Received: " + v); + return v.getBody(); + }; + } + + @Bean + public Function> inputOutputLBEventAsMessage() { + return v -> { + Message message = MessageBuilder.withPayload("\"Hello\"".getBytes(StandardCharsets.UTF_8)).setHeader("foo", "bar").build(); + return message; + }; + } + + @Bean + public Function inputOutputLBEvent() { + return v -> { + ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent(); + response.setBody(v.getBody()); + return response; + }; + } + + @Bean + public Function, String> inputLBEventAsMessage(JsonMapper jsonMapper) { + return message -> { + System.out.println("Received: " + message); + assertThat(message.getHeaders().get(AWSLambdaUtils.AWS_CONTEXT)).isNotNull(); + return message.getPayload().getBody(); + }; + } + + @Bean + public Function, String> inputLBEventAsMap() { return v -> { System.out.println("Received: " + v); return v.toString(); @@ -1161,6 +1414,14 @@ public Supplier supply() { return () -> "boom"; } + @Bean + public Function, Message> echoStringMessage() { + return m -> { + String encodedPayload = Base64.getEncoder().encodeToString(m.getPayload().getBytes(StandardCharsets.UTF_8)); + return MessageBuilder.withPayload(encodedPayload).setHeader("isBase64Encoded", true).build(); + }; + } + @Bean public Consumer consume() { @@ -1172,6 +1433,11 @@ public Function uppercase() { return v -> v.toUpperCase(); } + @Bean + public Function, Mono> reactiveWithVoidReturn() { + return v -> Mono.empty(); + } + @Bean public Function uppercasePojo() { return v -> { @@ -1179,6 +1445,15 @@ public Function uppercasePojo() { }; } + @Bean + public Function uppercasePojoReturnPojo() { + return v -> { + Person p = new Person(); + p.setName(v.getName().toUpperCase()); + return p; + }; + } + @Bean public Function inputApiEvent() { return v -> { @@ -1186,6 +1461,34 @@ public Function inputApiEvent() { }; } + @Bean + + public Function echoInputStreamToString() { + return is -> { + try { + String result = StreamUtils.copyToString(is, StandardCharsets.UTF_8); + return result; + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + + @Bean + + public Function, String> echoInputStreamMsgToString() { + return msg -> { + try { + String result = StreamUtils.copyToString(msg.getPayload(), StandardCharsets.UTF_8); + return result; + } + catch (Exception e) { + throw new RuntimeException(e); + } + }; + } + @Bean public Function inputOutputApiEvent() { return v -> { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandlerTests.java deleted file mode 100644 index c9aa2d935..000000000 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/SpringBootStreamHandlerTests.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.adapter.aws; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.util.Map; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.util.Assert; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Dave Syer - * @author Oleg Zhurakousky - */ -public class SpringBootStreamHandlerTests { - - private SpringBootStreamHandler handler; - - @BeforeEach - public void before() { - System.clearProperty("function.name"); - } - - @Test - public void functionBeanWithJacksonConfig() throws Exception { - this.handler = new SpringBootStreamHandler(FunctionConfigWithJackson.class); - this.handler.initialize(null); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - this.handler.handleRequest( - new ByteArrayInputStream("{\"value\":\"foo\"}".getBytes()), output, null); - assertThat(output.toString()).isEqualTo("{\"value\":\"FOO\"}"); - } - - @Test - public void functionBeanWithoutJacksonConfig() throws Exception { - this.handler = new SpringBootStreamHandler(FunctionConfigWithoutJackson.class); - this.handler.initialize(null); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - this.handler.handleRequest( - new ByteArrayInputStream("{\"value\":\"foo\"}".getBytes()), output, null); - assertThat(output.toString()).isEqualTo("{\"value\":\"FOO\"}"); - } - - @Test - public void functionNonFluxBeanNoCatalog() throws Exception { - this.handler = new SpringBootStreamHandler(NoCatalogNonFluxFunctionConfig.class); - this.handler.initialize(null); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - this.handler.handleRequest( - new ByteArrayInputStream("{\"value\":\"foo\"}".getBytes()), output, null); - assertThat(output.toString()).isEqualTo("{\"value\":\"FOO\"}"); - } - - @Test - public void functionFluxBeanNoCatalog() throws Exception { - this.handler = new SpringBootStreamHandler(NoCatalogFluxFunctionConfig.class); - this.handler.initialize(null); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - this.handler.handleRequest( - new ByteArrayInputStream("{\"value\":\"foo\"}".getBytes()), output, null); - assertThat(output.toString()).isEqualTo("{\"value\":\"FOO\"}"); - } - - @Test - public void typelessFunctionConfig() throws Exception { - this.handler = new SpringBootStreamHandler(TypelessFunctionConfig.class); - this.handler.initialize(null); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - this.handler.handleRequest( - new ByteArrayInputStream("{\"value\":\"foo\"}".getBytes()), output, null); - assertThat(output.toString()).isEqualTo("{\"value\":\"foo\"}"); - } - - @Test - public void inputStreamFunctionConfig() throws Exception { - this.handler = new SpringBootStreamHandler(InputStreamFunctionConfig.class); - this.handler.initialize(null); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - this.handler.handleRequest( - new ByteArrayInputStream("{\"value\":\"foo\"}".getBytes()), output, null); - assertThat(output.toString()).isEqualTo("{\"value\":\"FOO\"}"); - } - - @Configuration - protected static class NoCatalogNonFluxFunctionConfig { - - @Bean - public Function function() { - return foo -> new Bar(foo.getValue().toUpperCase()); - } - - } - - @Configuration - protected static class NoCatalogFluxFunctionConfig { - - @Bean - public Function, Flux> function() { - return flux -> flux.map(foo -> new Bar(foo.getValue().toUpperCase())); - } - - } - - @Configuration - @Import({ ContextFunctionCatalogAutoConfiguration.class, - JacksonAutoConfiguration.class }) - protected static class FunctionConfigWithJackson { - - @Bean - public Function function() { - return foo -> new Bar(foo.getValue().toUpperCase()); - } - - } - - @Configuration - @Import({ ContextFunctionCatalogAutoConfiguration.class }) - protected static class FunctionConfigWithoutJackson { - - @Bean - public Function function() { - return foo -> new Bar(foo.getValue().toUpperCase()); - } - - } - - @Configuration - @Import({ ContextFunctionCatalogAutoConfiguration.class, - JacksonAutoConfiguration.class }) - protected static class TypelessFunctionConfig { - - @Bean - public Function function() { - return value -> { - Assert.isTrue(value instanceof Map, "Expected value should be Map"); - return value; - }; - } - - } - - @Configuration - @Import({ ContextFunctionCatalogAutoConfiguration.class, - JacksonAutoConfiguration.class }) - protected static class InputStreamFunctionConfig { - - @Autowired - private ObjectMapper mapper; - - @Bean - public Function function() { - return value -> { - try { - Foo foo = this.mapper.readValue((InputStream) value, Foo.class); - return new Bar(foo.getValue().toUpperCase()); - } - catch (Exception e) { - throw new IllegalStateException("Failed test", e); - } - }; - } - - } - - protected static class Foo { - - private String value; - - public String getValue() { - return this.value; - } - - public void setValue(String value) { - this.value = value; - } - - } - - protected static class Bar { - - private String value; - - public Bar() { - } - - public Bar(String value) { - this.value = value; - } - - public String getValue() { - return this.value; - } - - public void setValue(String value) { - this.value = value; - } - - } - -} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/TestContext.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/TestContext.java new file mode 100644 index 000000000..6af6b78f7 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/TestContext.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.adapter.aws; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; + +/** + * + * @author Oleg Zhurakousky + * + */ +public class TestContext implements Context { + + @Override + public String getAwsRequestId() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getLogGroupName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getLogStreamName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getFunctionName() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getFunctionVersion() { + // TODO Auto-generated method stub + return null; + } + + @Override + public String getInvokedFunctionArn() { + // TODO Auto-generated method stub + return null; + } + + @Override + public CognitoIdentity getIdentity() { + // TODO Auto-generated method stub + return null; + } + + @Override + public ClientContext getClientContext() { + // TODO Auto-generated method stub + return null; + } + + @Override + public int getRemainingTimeInMillis() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public int getMemoryLimitInMB() { + // TODO Auto-generated method stub + return 0; + } + + @Override + public LambdaLogger getLogger() { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/pom.xml index 0b0ed1eab..6ae5c8e24 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/pom.xml @@ -1,7 +1,6 @@ + xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 spring-cloud-function-adapter-azure @@ -13,15 +12,15 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT UTF-8 UTF-8 1.8 - 1.2.2 - + 3.0.0 + 1.0.0 @@ -49,6 +48,12 @@ ${azure.functions.java.core.version} + + com.microsoft.azure.functions + azure-functions-java-spi + ${azure.functions.java.spi.version} + + diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureFunctionInstanceInjector.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureFunctionInstanceInjector.java new file mode 100644 index 000000000..3b35c923a --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureFunctionInstanceInjector.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.adapter.azure; + +import java.util.Map; + +import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.cloud.function.utils.FunctionClassUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +/** + * The instance factory used by the Spring framework to initialize Azure function instance. It is waived with the Azure + * Java Worker through the META-INFO/services/com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector service + * hook. The Azure Java Worker delegates scans the classpath for service definition and delegates the function class + * creation to this instance factory. + * @author Christian Tzolov + * @since 3.2.9 + */ +public class AzureFunctionInstanceInjector implements FunctionInstanceInjector { + + private static Log logger = LogFactory.getLog(AzureFunctionInstanceInjector.class); + + private static ConfigurableApplicationContext APPLICATION_CONTEXT; + + /** + * This method is called by the Azure Java Worker on every function invocation. The Worker sends in the classes + * annotated with @FunctionName annotations and allows the Spring framework to initialize the function instance as a + * Spring Bean and return the instance. Then the Worker uses the created instance to invoke the function. + * @param functionClass the class that contains customer Azure functions (e.g. @FunctionName annotated ) + * @param customer Azure functions class type + * @return the instance that will be invoked on by azure functions java worker + * @throws Exception any exception that is thrown by the Spring framework during instance creation + */ + @Override + public T getInstance(Class functionClass) throws Exception { + try { + // Backward compatibility workaround. If the function class is of type FunctionInvoker then create plain + // Java instance and delegate to FunctionInvoker adaptor approach. + if (ClassUtils.isAssignable(FunctionInvoker.class, functionClass)) { + return functionClass.newInstance(); + } + + initialize(FunctionClassUtils.getStartClass()); + Map azureFunctionBean = APPLICATION_CONTEXT.getBeansOfType(functionClass); + if (CollectionUtils.isEmpty(azureFunctionBean)) { + throw new IllegalStateException( + "Failed to retrieve Bean instance for: " + functionClass + + ". The class should be annotated with @Component to let the Spring framework initialize it!"); + } + return azureFunctionBean.entrySet().iterator().next().getValue(); + } + catch (Exception e) { + if (APPLICATION_CONTEXT != null) { + APPLICATION_CONTEXT.close(); + } + throw new IllegalStateException("Failed to initialize", e); + } + } + + /** + * Create a static Application Context instance shared between multiple function invocations. + */ + private static void initialize(Class springConfigurationClass) { + synchronized (AzureFunctionInstanceInjector.class.getName()) { + if (APPLICATION_CONTEXT == null) { + logger.info("Initializing: " + springConfigurationClass); + APPLICATION_CONTEXT = springApplication(springConfigurationClass).run(); + } + } + } + + private static SpringApplication springApplication(Class configurationClass) { + SpringApplication application = new org.springframework.cloud.function.context.FunctionalSpringApplication( + configurationClass); + application.setWebApplicationType(WebApplicationType.NONE); + return application; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java index 7ea1232a7..b73e2b148 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2021 the original author or authors. + * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -65,6 +65,7 @@ * @param input type * @param result type * @author Oleg Zhurakousky + * @author Chris Bono * @since 3.2 */ public class FunctionInvoker { @@ -120,37 +121,119 @@ else if (function == null && StringUtils.hasText(functionDefinition) && APPLICAT return function; } - @SuppressWarnings({ "unchecked", "rawtypes" }) public O handleRequest(I input, ExecutionContext executionContext) { String functionDefinition = executionContext.getFunctionName(); FunctionInvocationWrapper function = this.discoverFunction(functionDefinition); Object enhancedInput = enhanceInputIfNecessary(input, executionContext); - Object output = function.apply(enhancedInput); - if (output instanceof Publisher) { - if (FunctionTypeUtils.isMono(function.getOutputType())) { - return (O) this.convertOutputIfNecessary(input, Mono.from((Publisher) output).blockOptional().get()); + Object functionResult = function.apply(enhancedInput); + + if (functionResult instanceof Publisher) { + return postProcessReactiveFunctionResult(input, enhancedInput, (Publisher) functionResult, function, executionContext); + } + return postProcessImperativeFunctionResult(input, enhancedInput, functionResult, function, executionContext); + } + + /** + * Post-processes the result from a non-reactive function invocation before returning it to the Azure + * runtime. The default behavior is to {@link #convertOutputIfNecessary possibly convert} the result. + * + *

Provides a hook for custom function invokers to extend/modify the function results handling. + * + * @param rawInputs the inputs passed in from the Azure runtime + * @param functionInputs the actual inputs used for the function invocation; may be + * {@link #enhanceInputIfNecessary different} from the {@literal rawInputs} + * @param functionResult the result from the function invocation + * @param function the invoked function + * @param executionContext the Azure execution context + * @return the possibly modified function results + */ + @SuppressWarnings("unchecked") + protected O postProcessImperativeFunctionResult(I rawInputs, Object functionInputs, Object functionResult, + FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + return (O) this.convertOutputIfNecessary(rawInputs, functionResult); + } + + /** + * Post-processes the result from a reactive function invocation before returning it to the Azure + * runtime. The default behavior is to delegate to {@link #postProcessMonoFunctionResult} or + * {@link #postProcessFluxFunctionResult} based on the result type. + * + *

Provides a hook for custom function invokers to extend/modify the function results handling. + * + * @param rawInputs the inputs passed in from the Azure runtime + * @param functionInputs the actual inputs used for the function invocation; may be + * {@link #enhanceInputIfNecessary different} from the {@literal rawInputs} + * @param functionResult the result from the function invocation + * @param function the invoked function + * @param executionContext the Azure execution context + * @return the possibly modified function results + */ + protected O postProcessReactiveFunctionResult(I rawInputs, Object functionInputs, Publisher functionResult, + FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + if (FunctionTypeUtils.isMono(function.getOutputType())) { + return postProcessMonoFunctionResult(rawInputs, functionInputs, Mono.from(functionResult), function, executionContext); + } + return postProcessFluxFunctionResult(rawInputs, functionInputs, Flux.from(functionResult), function, executionContext); + } + + /** + * Post-processes the {@code Mono} result from a reactive function invocation before returning it to the Azure + * runtime. The default behavior is to {@link Mono#blockOptional()} and {@link #convertOutputIfNecessary possibly convert} the result. + * + *

Provides a hook for custom function invokers to extend/modify the function results handling. + * + * @param rawInputs the inputs passed in from the Azure runtime + * @param functionInputs the actual inputs used for the function invocation; may be + * {@link #enhanceInputIfNecessary different} from the {@literal rawInputs} + * @param functionResult the Mono result from the function invocation + * @param function the invoked function + * @param executionContext the Azure execution context + * @return the possibly modified function results + */ + @SuppressWarnings("unchecked") + protected O postProcessMonoFunctionResult(I rawInputs, Object functionInputs, Mono functionResult, + FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + return (O) this.convertOutputIfNecessary(rawInputs, functionResult.blockOptional().get()); + } + + /** + * Post-processes the {@code Flux} result from a reactive function invocation before returning it to the Azure + * runtime. The default behavior is to {@link Flux#toIterable() block} and {@link #convertOutputIfNecessary possibly convert} the results. + * + *

Provides a hook for custom function invokers to extend/modify the function results handling. + * + * @param rawInputs the inputs passed in from the Azure runtime + * @param functionInputs the actual inputs used for the function invocation; may be + * {@link #enhanceInputIfNecessary different} from the {@literal rawInputs} + * @param functionResult the Mono result from the function invocation + * @param function the invoked function + * @param executionContext the Azure execution context + * @return the possibly modified function results + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + protected O postProcessFluxFunctionResult(I rawInputs, Object functionInputs, Flux functionResult, + FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + List resultList = new ArrayList<>(); + for (Object resultItem : functionResult.toIterable()) { + if (resultItem instanceof Collection) { + resultList.addAll((Collection) resultItem); } else { - List resultList = new ArrayList<>(); - for (Object resultItem : Flux.from((Publisher) output).toIterable()) { - if (resultItem instanceof Collection) { - resultList.addAll((Collection) resultItem); - } - else { - if (!function.isSupplier() && Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getInputType())) - && !Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getOutputType()))) { - return (O) this.convertOutputIfNecessary(input, resultItem); - } - else { - resultList.add(resultItem); - } - } + if (!function.isSupplier() && Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getInputType())) + && !Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getOutputType()))) { + return (O) this.convertOutputIfNecessary(rawInputs, resultItem); + } + else { + resultList.add(resultItem); } - return (O) this.convertOutputIfNecessary(input, resultList); } } - return (O) this.convertOutputIfNecessary(input, output); + return (O) this.convertOutputIfNecessary(rawInputs, resultList); } @SuppressWarnings({ "unchecked", "rawtypes" }) diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/resources/META-INF/services/com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/resources/META-INF/services/com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector new file mode 100644 index 000000000..247910e32 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/resources/META-INF/services/com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector @@ -0,0 +1 @@ +org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/CustomFunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/CustomFunctionInvokerTests.java new file mode 100644 index 000000000..3d39543ec --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/CustomFunctionInvokerTests.java @@ -0,0 +1,200 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.adapter.azure; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.microsoft.azure.functions.ExecutionContext; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.support.GenericMessage; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.util.Lists.list; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +/** + * Unit tests for {@link FunctionInvoker} custom result handling. + * + * @author Chris Bono + */ +class CustomFunctionInvokerTests { + + private FunctionInvoker currentInvoker; + + @AfterEach + void closeCurrentInvoker() { + if (this.currentInvoker != null) { + this.currentInvoker.close(); + } + } + + /** + * Verifies custom result handling and proper post-process callback invocation for an imperative function. + */ + @Test + void customImperativeResultHandling() { + FunctionInvoker invoker = new FunctionInvoker(TestFunctionsConfig.class) { + @Override + protected String postProcessImperativeFunctionResult(String rawInputs, Object functionInputs, + Object functionResult, FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + return functionResult + "+imperative"; + } + }; + invoker = spyOnAndCloseAfterTest(invoker); + ExecutionContext executionContext = new TestExecutionContext("imperativeUppercase"); + String result = invoker.handleRequest("foo", executionContext); + assertThat(result).isEqualTo("FOO+imperative"); + + // Below here verifies that the expected callback(s) were invoked w/ the expected arguments + + // Only imperative post-process callback should be called + verify(invoker, never()).postProcessReactiveFunctionResult(anyString(), any(), any(Publisher.class), any(), same(executionContext)); + verify(invoker, never()).postProcessMonoFunctionResult(anyString(), any(), any(Mono.class), any(), same(executionContext)); + verify(invoker, never()).postProcessFluxFunctionResult(anyString(), any(), any(Flux.class), any(), same(executionContext)); + + // Only sniff-test the payload of the input message (the other fields are problematic to verify and no value doing that here) + ArgumentCaptor functionInputsCaptor = ArgumentCaptor.forClass(GenericMessage.class); + verify(invoker).postProcessImperativeFunctionResult(eq("foo"), functionInputsCaptor.capture(), eq("FOO"), any(), same(executionContext)); + assertThat(functionInputsCaptor.getValue()).extracting(GenericMessage::getPayload).isEqualTo("foo"); + } + + /** + * Verifies custom result handling and proper post-process callback invocation for a reactive Mono function. + */ + @Test + void customReactiveMonoResultHandling() { + FunctionInvoker invoker = new FunctionInvoker(TestFunctionsConfig.class) { + @Override + protected String postProcessMonoFunctionResult(String rawInputs, Object functionInputs, Mono functionResult, + FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + return functionResult.block().toString() + "+mono"; + } + }; + invoker = spyOnAndCloseAfterTest(invoker); + ExecutionContext executionContext = new TestExecutionContext("reactiveMonoUppercase"); + String result = invoker.handleRequest("foo", executionContext); + assertThat(result).isEqualTo("FOO+mono"); + + // Below here verifies that the expected callback(s) were invoked w/ the expected arguments + + // Only publisher->mono post-process callbacks should be called + verify(invoker, never()).postProcessImperativeFunctionResult(anyString(), any(), any(), any(), same(executionContext)); + verify(invoker, never()).postProcessFluxFunctionResult(anyString(), any(), any(Flux.class), any(), same(executionContext)); + + // Only sniff-test the payload of the input message and the mono (the other fields are problematic to verify and no value doing that here) + ArgumentCaptor functionInputsCaptor = ArgumentCaptor.forClass(GenericMessage.class); + ArgumentCaptor functionResultCaptor = ArgumentCaptor.forClass(Mono.class); + verify(invoker).postProcessReactiveFunctionResult(eq("foo"), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext)); + verify(invoker).postProcessMonoFunctionResult(eq("foo"), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext)); + // NOTE: The captors get called twice as the args are just delegated from publisher->mono callback + assertThat(functionInputsCaptor.getAllValues()).extracting(GenericMessage::getPayload).containsExactly("foo", "foo"); + assertThat(functionResultCaptor.getAllValues()).extracting(Mono::block).containsExactly("FOO", "FOO"); + } + + /** + * Verifies custom result handling and proper post-process callback invocation for a reactive Flux function. + */ + @Test + void customReactiveFluxResultHandling() { + FunctionInvoker, String> invoker = new FunctionInvoker, String>(TestFunctionsConfig.class) { + @Override + protected String postProcessFluxFunctionResult(List rawInputs, Object functionInputs, + Flux functionResult, FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + return functionResult.map(o -> o.toString() + "+flux").collectList().block().stream().collect(Collectors.joining("/")); + } + }; + invoker = spyOnAndCloseAfterTest(invoker); + ExecutionContext executionContext = new TestExecutionContext("reactiveFluxUppercase"); + List rawInputs = Arrays.asList("foo", "bar"); + String result = invoker.handleRequest(rawInputs, executionContext); + assertThat(result).isEqualTo("FOO+flux/BAR+flux"); + + // Below here verifies that the expected callback(s) were invoked w/ the expected arguments + + // Only publisher->flux post-process callbacks should be called + verify(invoker, never()).postProcessImperativeFunctionResult(anyList(), any(), any(), any(), same(executionContext)); + verify(invoker, never()).postProcessMonoFunctionResult(anyList(), any(), any(Mono.class), any(), same(executionContext)); + + // Only sniff-test the payload of the input message and the mono (the other fields are problematic to verify and no value doing that here) + ArgumentCaptor> functionInputsCaptor = ArgumentCaptor.forClass(Flux.class); + ArgumentCaptor functionResultCaptor = ArgumentCaptor.forClass(Flux.class); + verify(invoker).postProcessReactiveFunctionResult(same(rawInputs), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext)); + verify(invoker).postProcessFluxFunctionResult(same(rawInputs), functionInputsCaptor.capture(), functionResultCaptor.capture(), any(), same(executionContext)); + + // NOTE: The captors get called twice as the args are just delegated from publisher->flux callback + + // The functionInputs for each call is Flux with 2 items - one for 'foo' and one for 'bar' + assertThat(functionInputsCaptor.getAllValues()) + .extracting(Flux::collectList).extracting(Mono::block) + .flatExtracting(fluxAsList -> fluxAsList.stream().collect(Collectors.toList())) + .extracting(GenericMessage::getPayload).containsExactlyInAnyOrder("foo", "bar", "foo", "bar"); + + // The functionResult for each call is a Flux w/ 2 items { "FOO", "BAR" } + assertThat(functionResultCaptor.getAllValues()) + .extracting(Flux::collectList).extracting(Mono::block) + .containsExactlyInAnyOrder(list("FOO", "BAR"), list("FOO", "BAR")); + } + + private FunctionInvoker spyOnAndCloseAfterTest(FunctionInvoker invoker) { + this.currentInvoker = invoker; + return spy(invoker); + } + + @Configuration + @EnableAutoConfiguration + static class TestFunctionsConfig { + + @Bean + public Function imperativeUppercase() { + return (s) -> s.toUpperCase(); + } + + @Bean + public Function, Mono> reactiveMonoUppercase() { + return (m) -> m.map(String::toUpperCase); + } + + @Bean + public Function, Flux> reactiveFluxUppercase() { + return (f) -> f.map(String::toUpperCase); + } + + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml index f348aac6a..41df7e6e1 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/pom.xml @@ -11,7 +11,7 @@ spring-cloud-function-adapter-parent org.springframework.cloud - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT @@ -68,6 +68,7 @@ com.github.blindpirate junit5-capture-system-output-extension 0.1.2 + test diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk/pom.xml index a7fbb0e60..d3a32847d 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-openwhisk/pom.xml @@ -13,7 +13,7 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml index 3e38496f9..5735ac26d 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc-cloudevent-ext/pom.xml @@ -5,13 +5,13 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT spring-cloud-function-grpc-cloudevent-ext spring-cloud-function-grpc-cloudevent-ext CloudEvent extension for spring-cloud-function-grpc - 1.8 + 1.55.1 @@ -23,7 +23,11 @@ spring-cloud-function-grpc ${project.version} - + + io.grpc + grpc-stub + ${grpc.version} + org.springframework.boot spring-boot-starter-test @@ -41,16 +45,26 @@ + org.apache.maven.plugins + maven-checkstyle-plugin + + + checkstyle-validation + none + + + + org.xolstice.maven.plugins protobuf-maven-plugin 0.6.1 - com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier} + com.google.protobuf:protoc:3.23.0:exe:${os.detected.classifier} grpc-java - io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier} + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} @@ -64,41 +78,4 @@ - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - - - - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - false - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - false - - - - diff --git a/spring-cloud-function-adapters/spring-cloud-function-grpc/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-grpc/pom.xml index 2dbdff528..067b318fc 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-grpc/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc/pom.xml @@ -12,34 +12,38 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT - 1.16.1 + 1.55.1 true - - io.grpc - grpc-netty - ${grpc.version} + javax.annotation + javax.annotation-api + 1.3.2 + + + io.grpc + grpc-netty + ${grpc.version} - io.grpc - grpc-protobuf - ${grpc.version} + io.grpc + grpc-protobuf + ${grpc.version} - io.grpc - grpc-services - ${grpc.version} + io.grpc + grpc-services + ${grpc.version} - io.grpc - grpc-stub - ${grpc.version} + io.grpc + grpc-stub + ${grpc.version} org.springframework.cloud @@ -55,44 +59,47 @@ spring-boot-starter-test test + + org.awaitility + awaitility + test + - - - kr.motd.maven - os-maven-plugin - 1.6.1 - + + kr.motd.maven + os-maven-plugin + 1.6.1 + - - org.apache.maven.plugins - maven-checkstyle-plugin - + org.apache.maven.plugins + maven-checkstyle-plugin + - org.xolstice.maven.plugins - protobuf-maven-plugin - 0.6.1 - - - com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier} + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + com.google.protobuf:protoc:3.23.0:exe:${os.detected.classifier} - grpc-java - - io.grpc:protoc-gen-grpc-java:1.4.0:exe:${os.detected.classifier} + grpc-java + + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} - - - - - compile - compile-custom - - - - + + + + + compile + compile-custom + + + + diff --git a/spring-cloud-function-compiler/.jdk8 b/spring-cloud-function-compiler/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/spring-cloud-function-compiler/pom.xml b/spring-cloud-function-compiler/pom.xml index 602f0449d..3c47d5d3f 100644 --- a/spring-cloud-function-compiler/pom.xml +++ b/spring-cloud-function-compiler/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/AbstractFunctionCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/AbstractFunctionCompiler.java deleted file mode 100644 index e9f1179be..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/AbstractFunctionCompiler.java +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.util.List; -import java.util.regex.Matcher; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.cloud.function.compiler.java.CompilationFailedException; -import org.springframework.cloud.function.compiler.java.CompilationMessage; -import org.springframework.cloud.function.compiler.java.CompilationResult; -import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * @param result type - * @author Andy Clement - * @author Mark Fisher - * @author Oleg Zhurakousky - */ -public abstract class AbstractFunctionCompiler { - - // Newlines in the property are escaped - private static final String NEWLINE_ESCAPE = Matcher.quoteReplacement("\\n"); - - // Individual double-quote characters are represented by two double quotes in the DSL - private static final String DOUBLE_DOUBLE_QUOTE = Matcher.quoteReplacement("\"\""); - - private static Logger logger = LoggerFactory - .getLogger(AbstractFunctionCompiler.class); - - /** - * The user supplied code snippet is inserted into the template and then the result is - * compiled. - */ - // @formatter:off - private static String SOURCE_CODE_TEMPLATE = "package " - + AbstractFunctionCompiler.class.getPackage().getName() + ";\n" - + "import java.util.*;\n" // Helpful to include this - + "import java.time.*;\n" // Helpful to include this - + "import java.util.function.*;\n" - + "import reactor.core.publisher.Flux;\n" - + "public class %s implements CompilationResultFactory<%s> {\n" - + " public %s<%s> getResult() {\n" - + " %s\n" - + " }\n" - + "}\n"; - // @formatter:on - private final ResultType resultType; - - private final String[] defaultResultTypeParameterizations; - - private final RuntimeJavaCompiler compiler = new RuntimeJavaCompiler(); - - AbstractFunctionCompiler(ResultType type, - String... defaultResultTypeParameterizations) { - this.resultType = type; - this.defaultResultTypeParameterizations = defaultResultTypeParameterizations; - } - - private static String decode(String input) { - return input.replaceAll(NEWLINE_ESCAPE, "\n").replaceAll(DOUBLE_DOUBLE_QUOTE, - "\""); - } - - /** - * Produce a factory instance by: - *

    - *
  • Decoding the code String to process any newlines/double-double-quotes - *
  • Insert the code into the source code template for a class - *
  • Compiling the class using the JDK provided Java Compiler - *
  • Loading the compiled class - *
  • Invoking a well known method on the factory class to produce a Consumer, - * Function, or Supplier instance - *
  • Returning that instance. - *
- * @param name - name of the function - * @param code - code of the function - * @param resultTypeParameterizations - result types - * @return a factory instance - */ - public final CompiledFunctionFactory compile(String name, String code, - String... resultTypeParameterizations) { - if (name == null || name.length() == 0) { - throw new IllegalArgumentException("name must not be empty"); - } - logger.info("Initial code property value :'{}'", code); - String[] parameterizedTypes = (!ObjectUtils.isEmpty(resultTypeParameterizations)) - ? resultTypeParameterizations : this.defaultResultTypeParameterizations; - code = decode(code); - if (code.startsWith("\"") && code.endsWith("\"")) { - code = code.substring(1, code.length() - 1); - } - if (!code.startsWith("return ") && !code.endsWith(";")) { - code = String.format("return (%s<%s> & java.io.Serializable) %s;", - this.resultType, - StringUtils.arrayToCommaDelimitedString(parameterizedTypes), code); - } - logger.info("Processed code property value :\n{}\n", code); - String firstLetter = name.substring(0, 1).toUpperCase(); - name = (name.length() > 1) ? firstLetter + name.substring(1) : firstLetter; - String className = String.format("%s.%s%sFactory", - this.getClass().getPackage().getName(), name, this.resultType); - CompilationResult compilationResult = buildAndCompileSourceCode(className, code, - parameterizedTypes); - if (compilationResult.wasSuccessful()) { - CompiledFunctionFactory factory = new CompiledFunctionFactory<>(className, - compilationResult); - return this.postProcessCompiledFunctionFactory(factory); - } - List compilationMessages = compilationResult - .getCompilationMessages(); - throw new CompilationFailedException(compilationMessages); - } - - /** - * Implementing subclasses may override this, e.g. to set the input and/or output - * types. - * @param factory the {@link CompiledFunctionFactory} produced by - * {@link #compile(String, String, String...)} - * @return the post-processed {@link CompiledFunctionFactory} - */ - protected CompiledFunctionFactory postProcessCompiledFunctionFactory( - CompiledFunctionFactory factory) { - return factory; - } - - /** - * Create the source for and then compile and load a class that embodies the supplied - * methodBody. The methodBody is inserted into a class template that returns the - * specified parameterized type. This method can return more than one class if the - * method body includes local class declarations. An example methodBody would be - * return input -> input.buffer(5).map(list->list.get(0));. - * @param className the name of the class - * @param methodBody the source code for a method - * @param parameterizedTypes the array of String representations for the parameterized - * input and/or output types, e.g.: <Flux<Object>> - * @return the list of Classes produced by compiling and then loading the snippet of - * code - */ - private CompilationResult buildAndCompileSourceCode(String className, - String methodBody, String[] parameterizedTypes) { - String sourceCode = makeSourceClassDefinition(className, methodBody, - parameterizedTypes); - return this.compiler.compile(className, sourceCode); - } - - /** - * Make a full source code definition for a class by applying the specified method - * body to the Reactive template. - * @param className the name of the class - * @param methodBody the code to insert into the Reactive source class template - * @param types the parameterized input and/or output types as Strings - * @return a complete Java Class definition - */ - private String makeSourceClassDefinition(String className, String methodBody, - String[] types) { - String shortClassName = className.substring(className.lastIndexOf('.') + 1); - String s = String.format(SOURCE_CODE_TEMPLATE, shortClassName, this.resultType, - this.resultType, StringUtils.arrayToCommaDelimitedString(types), - methodBody); - logger.info("\n" + s); - return s; - } - - enum ResultType { - - Consumer, Function, Supplier - - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompilationResultFactory.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompilationResultFactory.java deleted file mode 100644 index 1f643b80b..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompilationResultFactory.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -/** - * @param result type - * @author Mark Fisher - */ -public interface CompilationResultFactory { - - T getResult(); - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompiledFunctionFactory.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompiledFunctionFactory.java deleted file mode 100644 index 0083b2fdc..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/CompiledFunctionFactory.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.concurrent.atomic.AtomicReference; - -import org.springframework.cloud.function.compiler.java.CompilationResult; -import org.springframework.util.ReflectionUtils; - -/** - * @param result type - * @author Mark Fisher - */ -public class CompiledFunctionFactory implements CompilationResultFactory { - - private final T result; - - private final byte[] generatedClassBytes; - - private String inputType; - - private String outputType; - - private Method method; - - public CompiledFunctionFactory(String className, - CompilationResult compilationResult) { - List> clazzes = compilationResult.getCompiledClasses(); - T result = null; - Method method = null; - for (Class clazz : clazzes) { - if (clazz.getName().equals(className)) { - try { - @SuppressWarnings("unchecked") - CompilationResultFactory factory = (CompilationResultFactory) clazz - .newInstance(); - result = factory.getResult(); - method = findFactoryMethod(clazz); - } - catch (Exception e) { - throw new IllegalArgumentException( - "Unexpected problem during retrieval of Function from compiled class", - e); - } - } - } - if (result == null) { - throw new IllegalArgumentException("Failed to extract compilation result."); - } - this.result = result; - this.method = method; - this.generatedClassBytes = compilationResult.getClassBytes(className); - } - - private Method findFactoryMethod(Class clazz) { - AtomicReference method = new AtomicReference<>(); - ReflectionUtils.doWithLocalMethods(clazz, m -> { - if (m.getName().equals("getResult") - && m.getReturnType().getName().startsWith("java.util.function")) { - method.set(m); - } - }); - return method.get(); - } - - public T getResult() { - return this.result; - } - - public Method getFactoryMethod() { - return this.method; - } - - public String getInputType() { - return this.inputType; - } - - public void setInputType(String inputType) { - this.inputType = inputType; - } - - public String getOutputType() { - return this.outputType; - } - - public void setOutputType(String outputType) { - this.outputType = outputType; - } - - public byte[] getGeneratedClassBytes() { - return this.generatedClassBytes; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/ConsumerCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/ConsumerCompiler.java deleted file mode 100644 index e098f9049..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/ConsumerCompiler.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.util.function.Consumer; - -/** - * @param result type - * @author Mark Fisher - */ -public class ConsumerCompiler extends AbstractFunctionCompiler> { - - private final String inputType; - - public ConsumerCompiler() { - this("Flux"); - } - - public ConsumerCompiler(String inputType) { - super(ResultType.Consumer, inputType); - this.inputType = inputType; - } - - @Override - protected CompiledFunctionFactory> postProcessCompiledFunctionFactory( - CompiledFunctionFactory> factory) { - factory.setInputType(this.inputType); - return factory; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java deleted file mode 100644 index ad3ce77dd..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/FunctionCompiler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.util.function.Function; - -/** - * @param function input type - * @param function output type - * @author Mark Fisher - */ -public class FunctionCompiler extends AbstractFunctionCompiler> { - - private final String inputType; - - private final String outputType; - - public FunctionCompiler() { - this("Flux"); - } - - public FunctionCompiler(String type) { - this(type, type); - } - - public FunctionCompiler(String inputType, String outputType) { - super(ResultType.Function, inputType, outputType); - this.inputType = inputType; - this.outputType = outputType; - } - - @Override - protected CompiledFunctionFactory> postProcessCompiledFunctionFactory( - CompiledFunctionFactory> factory) { - factory.setInputType(this.inputType); - factory.setOutputType(this.outputType); - - return factory; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/SupplierCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/SupplierCompiler.java deleted file mode 100644 index 57ab191bd..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/SupplierCompiler.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.util.function.Supplier; - -/** - * @param input type - * @author Mark Fisher - */ -public class SupplierCompiler extends AbstractFunctionCompiler> { - - private final String outputType; - - public SupplierCompiler() { - this("Flux"); - } - - public SupplierCompiler(String outputType) { - super(ResultType.Supplier, outputType); - this.outputType = outputType; - } - - @Override - protected CompiledFunctionFactory> postProcessCompiledFunctionFactory( - CompiledFunctionFactory> factory) { - factory.setOutputType(this.outputType); - return factory; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompiledFunctionRegistry.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompiledFunctionRegistry.java deleted file mode 100644 index 3717514d5..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompiledFunctionRegistry.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.app; - -import java.io.File; -import java.io.IOException; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import reactor.core.publisher.Flux; - -import org.springframework.cloud.function.compiler.AbstractFunctionCompiler; -import org.springframework.cloud.function.compiler.CompiledFunctionFactory; -import org.springframework.cloud.function.compiler.ConsumerCompiler; -import org.springframework.cloud.function.compiler.FunctionCompiler; -import org.springframework.cloud.function.compiler.SupplierCompiler; -import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; - -/** - * @author Mark Fisher - * @author Oleg Zhurakousky - */ -public class CompiledFunctionRegistry { - - private static final String SUPPLIER_DIRECTORY = "suppliers"; - - private static final String FUNCTION_DIRECTORY = "functions"; - - private static final String CONSUMER_DIRECTORY = "consumers"; - - private final File supplierDirectory; - - private final File functionDirectory; - - private final File consumerDirectory; - - private final AbstractFunctionCompiler>> supplierCompiler = new SupplierCompiler<>(); - - private final AbstractFunctionCompiler, Flux>> functionCompiler = new FunctionCompiler<>(); - - private final AbstractFunctionCompiler>> consumerCompiler = new ConsumerCompiler<>(); - - public CompiledFunctionRegistry() { - this(new File("/tmp/function-registry")); - } - - public CompiledFunctionRegistry(File directory) { - Assert.notNull(directory, "Directory must not be null"); - if (!directory.exists()) { - directory.mkdirs(); - } - else { - Assert.isTrue(directory.isDirectory(), - String.format("%s is not a directory.", directory.getAbsolutePath())); - } - this.supplierDirectory = new File(directory, SUPPLIER_DIRECTORY); - this.functionDirectory = new File(directory, FUNCTION_DIRECTORY); - this.consumerDirectory = new File(directory, CONSUMER_DIRECTORY); - this.supplierDirectory.mkdir(); - this.functionDirectory.mkdir(); - this.consumerDirectory.mkdir(); - } - - public void registerSupplier(String name, String lambda, String type) { - this.doRegister(this.supplierCompiler, this.supplierDirectory, name, lambda, - type); - } - - public void registerFunction(String name, String lambda, String... types) { - this.doRegister(this.functionCompiler, this.functionDirectory, name, lambda, - types); - } - - public void registerConsumer(String name, String lambda, String type) { - this.doRegister(this.consumerCompiler, this.consumerDirectory, name, lambda, - type); - } - - private void doRegister(AbstractFunctionCompiler compiler, File directory, - String name, String lambda, String... types) { - CompiledFunctionFactory factory = compiler.compile(name, lambda, types); - File file = new File(directory, fileName(name)); - try { - FileCopyUtils.copy(factory.getGeneratedClassBytes(), file); - } - catch (IOException e) { - throw new IllegalArgumentException( - String.format("failed to register '%s'", name), e); - } - } - - private String fileName(String functionName) { - return String.format("%s.%s", functionName, "fun"); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerController.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerController.java deleted file mode 100644 index ca949d66f..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerController.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.app; - -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -/** - * @author Mark Fisher - */ -@RestController -public class CompilerController { - - private final CompiledFunctionRegistry registry = new CompiledFunctionRegistry(); - - @PostMapping(path = "/supplier/{name}") - public void registerSupplier(@PathVariable String name, @RequestBody String lambda, - @RequestParam(defaultValue = "Flux") String type) { - this.registry.registerSupplier(name, lambda, type); - } - - @PostMapping(path = "/function/{name}") - public void registerFunction(@PathVariable String name, @RequestBody String lambda, - @RequestParam(defaultValue = "Flux") String inputType, - @RequestParam(defaultValue = "Flux") String outputType) { - this.registry.registerFunction(name, lambda, inputType, outputType); - } - - @PostMapping(path = "/consumer/{name}") - public void registerConsumer(@PathVariable String name, @RequestBody String lambda, - @RequestParam(defaultValue = "Flux") String type) { - this.registry.registerConsumer(name, lambda, type); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/config/FunctionProxyApplicationListener.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/config/FunctionProxyApplicationListener.java deleted file mode 100644 index 1d93c74b0..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/config/FunctionProxyApplicationListener.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.config; - -import java.util.HashMap; -import java.util.Map; - -import org.springframework.beans.MutablePropertyValues; -import org.springframework.beans.factory.config.ConstructorArgumentValues; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.boot.context.event.ApplicationPreparedEvent; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; -import org.springframework.cloud.function.compiler.ConsumerCompiler; -import org.springframework.cloud.function.compiler.FunctionCompiler; -import org.springframework.cloud.function.compiler.SupplierCompiler; -import org.springframework.cloud.function.compiler.proxy.ByteCodeLoadingConsumer; -import org.springframework.cloud.function.compiler.proxy.ByteCodeLoadingFunction; -import org.springframework.cloud.function.compiler.proxy.ByteCodeLoadingSupplier; -import org.springframework.cloud.function.compiler.proxy.LambdaCompilingConsumer; -import org.springframework.cloud.function.compiler.proxy.LambdaCompilingFunction; -import org.springframework.cloud.function.compiler.proxy.LambdaCompilingSupplier; -import org.springframework.context.ApplicationListener; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.io.ByteArrayResource; -import org.springframework.core.io.Resource; -import org.springframework.util.Assert; - -/** - * @author Mark Fisher - */ -@ConfigurationProperties("spring.cloud.function") -public class FunctionProxyApplicationListener - implements ApplicationListener { - - private final SupplierCompiler supplierCompiler = new SupplierCompiler<>(); - - private final FunctionCompiler functionCompiler = new FunctionCompiler<>(); - - private final ConsumerCompiler consumerCompiler = new ConsumerCompiler<>(); - - /** - * Configuration for function bodies, which will be compiled. The key in the map is - * the function name and the value is a map containing a key "lambda" which is the - * body to compile, and optionally a "type" (defaults to "function"). Can also contain - * "inputType" and "outputType" in case it is ambiguous. - */ - private final Map compile = new HashMap<>(); - - /** - * Configuration for a set of files containing function bodies, which will be imported - * and compiled. The key in the map is the function name and the value is another map, - * containing a "location" of the file to compile and (optionally) a "type" (defaults - * to "function"). - */ - private final Map imports = new HashMap<>(); - - public Map getCompile() { - return this.compile; - } - - public Map getImports() { - return this.imports; - } - - @Override - public void onApplicationEvent(ApplicationPreparedEvent event) { - ConfigurableApplicationContext context = event.getApplicationContext(); - DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context - .getBeanFactory(); - bind(context, beanFactory); - for (Map.Entry entry : this.compile.entrySet()) { - String name = entry.getKey(); - @SuppressWarnings("unchecked") - Map properties = (Map) entry.getValue(); - String type = (properties.get("type") != null) ? properties.get("type") - : "function"; - String lambda = properties.get("lambda"); - Assert.notNull(lambda, () -> String.format( - "The 'lambda' property is required for compiling Function: %s", - name)); - String inputType = properties.get("inputType"); - String outputType = properties.get("outputType"); - registerLambdaCompilingProxy(name, type, inputType, outputType, lambda, - beanFactory); - } - for (Map.Entry entry : this.imports.entrySet()) { - String name = entry.getKey(); - @SuppressWarnings("unchecked") - Map properties = (Map) entry.getValue(); - String type = (properties.get("type") != null) ? properties.get("type") - : "function"; - String location = properties.get("location"); - Assert.notNull(location, String.format( - "The 'location' property is required for importing Function: %s", - name)); - registerByteCodeLoadingProxy(name, type, context.getResource(location), - beanFactory); - } - } - - private void bind(ConfigurableApplicationContext application, - DefaultListableBeanFactory context) { - Binder.get(application.getEnvironment()).bind("spring.cloud.function", - Bindable.ofInstance(this)); - } - - private void registerByteCodeLoadingProxy(String name, String type, Resource resource, - DefaultListableBeanFactory beanFactory) { - Class proxyClass = null; - if ("supplier".equals(type.toLowerCase())) { - proxyClass = ByteCodeLoadingSupplier.class; - } - else if ("consumer".equals(type.toLowerCase())) { - proxyClass = ByteCodeLoadingConsumer.class; - } - else { - proxyClass = ByteCodeLoadingFunction.class; - } - RootBeanDefinition beanDefinition = new RootBeanDefinition(proxyClass); - ConstructorArgumentValues args = new ConstructorArgumentValues(); - args.addGenericArgumentValue(resource); - beanDefinition.setConstructorArgumentValues(args); - beanFactory.registerBeanDefinition(name, beanDefinition); - } - - private void registerLambdaCompilingProxy(String name, String type, String inputType, - String outputType, String lambda, DefaultListableBeanFactory beanFactory) { - Resource resource = new ByteArrayResource(lambda.getBytes()); - ConstructorArgumentValues args = new ConstructorArgumentValues(); - MutablePropertyValues props = new MutablePropertyValues(); - args.addGenericArgumentValue(resource); - Class proxyClass = null; - if ("supplier".equals(type.toLowerCase())) { - proxyClass = LambdaCompilingSupplier.class; - args.addGenericArgumentValue(this.supplierCompiler); - if (outputType != null) { - props.add("typeParameterizations", outputType); - } - } - else if ("consumer".equals(type.toLowerCase())) { - proxyClass = LambdaCompilingConsumer.class; - args.addGenericArgumentValue(this.consumerCompiler); - if (inputType != null) { - props.add("typeParameterizations", inputType); - } - } - else { - proxyClass = LambdaCompilingFunction.class; - args.addGenericArgumentValue(this.functionCompiler); - if ((inputType == null && outputType != null) - || (outputType == null && inputType != null)) { - throw new IllegalArgumentException( - "if either input or output type is set, the other is also required"); - } - if (inputType != null) { - props.add("typeParameterizations", - new String[] { inputType, outputType }); - } - } - RootBeanDefinition beanDefinition = new RootBeanDefinition(proxyClass); - beanDefinition.setConstructorArgumentValues(args); - beanDefinition.setPropertyValues(props); - beanFactory.registerBeanDefinition(name, beanDefinition); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CloseableFilterableJavaFileObjectIterable.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CloseableFilterableJavaFileObjectIterable.java deleted file mode 100644 index 80605e030..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CloseableFilterableJavaFileObjectIterable.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; - -import javax.tools.JavaFileObject; - -import org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager.CompilationInfoCache; - -/** - * Common superclass for iterables that need to handle closing when finished with and that - * need to handle possible constraints on the values that are iterated over. - * - * @author Andy Clement - */ -public abstract class CloseableFilterableJavaFileObjectIterable - implements Iterable { - - // private final static Logger logger = - // LoggerFactory.getLogger(CloseableFilterableJavaFileObjectIterable.class); - - private final static boolean BOOT_PACKAGING_AWARE = true; - - private final static String BOOT_PACKAGING_PREFIX_FOR_CLASSES = "BOOT-INF/classes/"; - - // If set specifies the package the iterator consumer is interested in. Only - // return results in this package. Will have a trailing separator to speed - // matching. '/' on its own represents the default package - protected String packageNameFilter; - - // Indicates whether the consumer of the iterator wants to see classes - // that are in subpackages of those matching the filter. - protected boolean includeSubpackages; - - protected CompilationInfoCache compilationInfoCache; - - public CloseableFilterableJavaFileObjectIterable( - CompilationInfoCache compilationInfoCache, String packageNameFilter, - boolean includeSubpackages) { - if (packageNameFilter != null && packageNameFilter.contains(File.separator)) { - throw new IllegalArgumentException( - "Package name filters should use dots to separate components: " - + packageNameFilter); - } - this.compilationInfoCache = compilationInfoCache; - // Normalize filter to forward slashes - this.packageNameFilter = packageNameFilter == null ? null - : packageNameFilter.replace('.', '/') + '/'; - this.includeSubpackages = includeSubpackages; - } - - /** - * Used by subclasses to check values against any specified constraints. - * @param name the name to check against the criteria - * @return true if the name is a valid iterator result based on the specified criteria - */ - protected boolean accept(String name) { - // logger.debug("checking {} against constraints packageNameFilter={} - // includeSubpackages={}",name,packageNameFilter,includeSubpackages); - if (!name.endsWith(".class")) { - return false; - } - if (this.packageNameFilter == null) { - return true; - } - boolean accept; - // Normalize to forward slashes (some jars are producing paths with forward - // slashes, some with backward slashes) - name = name.replace('\\', '/'); - if (this.packageNameFilter.length() == 1 && this.packageNameFilter.equals("/")) { - // This is the 'default package' filter representation - if (name.indexOf('/') == -1) { - accept = true; - } - else if (BOOT_PACKAGING_AWARE) { - accept = name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES) && name - .indexOf('/', BOOT_PACKAGING_PREFIX_FOR_CLASSES.length()) == -1; - } - return accept; - } - if (this.includeSubpackages) { - accept = name.startsWith(this.packageNameFilter); - if (!accept && BOOT_PACKAGING_AWARE) { - accept = name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES) - && name.indexOf( - this.packageNameFilter) == BOOT_PACKAGING_PREFIX_FOR_CLASSES - .length(); - } - } - else { - accept = name.startsWith(this.packageNameFilter) - && name.indexOf("/", this.packageNameFilter.length()) == -1; - if (!accept && BOOT_PACKAGING_AWARE) { - accept = name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES) - && name.indexOf( - this.packageNameFilter) == BOOT_PACKAGING_PREFIX_FOR_CLASSES - .length() - && name.indexOf("/", BOOT_PACKAGING_PREFIX_FOR_CLASSES.length() - + this.packageNameFilter.length()) == -1; - } - } - return accept; - } - - abstract void close(); - - abstract void reset(); - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationFailedException.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationFailedException.java deleted file mode 100644 index 503e7a0c6..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationFailedException.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.util.List; - -/** - * @author Mark Fisher - */ -@SuppressWarnings("serial") -public class CompilationFailedException extends RuntimeException { - - public CompilationFailedException(List messages) { - super(consolidateMessages(messages)); - } - - private static String consolidateMessages(List messages) { - if (messages == null || messages.isEmpty()) { - return ""; - } - StringBuilder sb = new StringBuilder(); - for (CompilationMessage message : messages) { - sb.append(message.toString()); - } - return sb.toString(); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationMessage.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationMessage.java deleted file mode 100644 index bd3bcc933..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationMessage.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -/** - * Encapsulate information produced during compilation. A message may be an error or - * something less serious (warning/informational). The toString() method will - * produce a formatted error include source context indicating the precise location of the - * problem. - * - * @author Andy Clement - */ -public class CompilationMessage { - - private Kind kind; - - private String message; - - private String sourceCode; - - private int startPosition; - - private int endPosition; - - public CompilationMessage(Kind kind, String message, String sourceCode, - int startPosition, int endPosition) { - this.kind = kind; - this.message = message; - this.sourceCode = sourceCode; - this.startPosition = startPosition; - this.endPosition = endPosition; - } - - /** - * @return the type of message - */ - public Kind getKind() { - return this.kind; - } - - /** - * @return the message text - */ - public String getMessage() { - return this.message; - } - - /** - * @return the source code for the file associated with the message - */ - public String getSourceCode() { - return this.sourceCode; - } - - /** - * @return offset from start of source file where the error begins - */ - public int getStartPosition() { - return this.startPosition; - } - - /** - * @return offset from start of source file where the error ends - */ - public int getEndPosition() { - return this.endPosition; - } - - public String toString() { - StringBuilder s = new StringBuilder(); - s.append("==========\n"); - if (this.sourceCode != null) { // Cannot include source context if no source - // available - int[] lineStartEnd = getLineStartEnd(this.startPosition); - s.append(this.sourceCode.substring(lineStartEnd[0], lineStartEnd[1])) - .append("\n"); - int col = lineStartEnd[0]; - // When inserting the whitespace, ensure tabs in the source line are respected - while ((col) < this.startPosition) { - s.append(this.sourceCode.charAt(col++) == '\t' ? "\t" : " "); - } - // Want at least one ^ - s.append("^"); - col++; - while ((col++) < this.endPosition) { - s.append("^"); - } - s.append("\n"); - } - s.append(this.kind).append(":").append(this.message).append("\n"); - s.append("==========\n"); - return s.toString(); - } - - /** - * For a given position in the source code this method returns a pair of int that - * indicate the start and end of the line within the source code that contain the - * position. - * @param searchPos the position of interest in the source code - * @return an int array of length 2 containing the start and end positions of the line - */ - private int[] getLineStartEnd(int searchPos) { - int previousPos = -1; - int pos = 0; - do { - pos = this.sourceCode.indexOf('\n', previousPos + 1); - if (searchPos < pos) { - return new int[] { previousPos + 1, pos }; - } - previousPos = pos; - } - while (pos != -1); - return new int[] { previousPos + 1, this.sourceCode.length() }; - } - - enum Kind { - - ERROR, OTHER - - } - // TODO test coverage for first line/last line situations - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationOutputCollector.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationOutputCollector.java deleted file mode 100644 index 2dfe424b6..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationOutputCollector.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.util.ArrayList; -import java.util.List; - -import javax.tools.FileObject; -import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject.Kind; - -/** - * During compilation instances of this class will collect up the output files from the - * compilation process. Any kind of file is collected upon but access is only currently - * provided to retrieve classes produced during compilation. Annotation processors that - * run may create other kinds of artifact. - * - * @author Andy Clement - */ -public class CompilationOutputCollector { - - private List outputFiles = new ArrayList<>(); - - /** - * Retrieve compiled classes that have been collected since this collector was built. - * Due to annotation processing it is possible other source files or metadata files - * may be produced during compilation - those are not included in the returned list. - * @return list of compiled classes - */ - public List getCompiledClasses() { - List compiledClassDefinitions = new ArrayList<>(); - for (InMemoryJavaFileObject outputFile : this.outputFiles) { - if (outputFile.getKind() == Kind.CLASS) { - CompiledClassDefinition compiledClassDefinition = new CompiledClassDefinition( - outputFile.getName(), outputFile.getBytes()); - compiledClassDefinitions.add(compiledClassDefinition); - } - } - return compiledClassDefinitions; - } - - public InMemoryJavaFileObject getJavaFileForOutput(Location location, - String className, Kind kind, FileObject sibling) { - InMemoryJavaFileObject jfo = InMemoryJavaFileObject.getJavaFileObject(location, - className, kind, sibling); - this.outputFiles.add(jfo); - return jfo; - } - - public InMemoryJavaFileObject getFileForOutput(Location location, String packageName, - String relativeName, FileObject sibling) { - InMemoryJavaFileObject ojfo = InMemoryJavaFileObject.getFileObject(location, - packageName, relativeName, sibling); - this.outputFiles.add(ojfo); - return ojfo; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java deleted file mode 100644 index 373ecb80c..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompilationResult.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Holder for the results of compilation. If compilation was successful the set of classes - * that resulted from compilation will be available. If compilation was not successful the - * error messages should provide information about why. Note that compilation may succeed - * and yet there will still be informational or warning messages collected. - * - * @author Andy Clement - * @author Mark Fisher - */ -public class CompilationResult { - - List compilationMessages = new ArrayList<>(); - - List> compiledClasses = new ArrayList<>(); - - private boolean successfulCompilation; - - private Map classBytes = new HashMap<>(); - - private List resolvedAdditionalDependencies = new ArrayList<>(); - - public CompilationResult(boolean successfulCompilation) { - this.successfulCompilation = successfulCompilation; - } - - public void addClassBytes(String name, byte[] bytes) { - this.classBytes.put(name, bytes); - } - - public List getResolvedAdditionalDependencies() { - return this.resolvedAdditionalDependencies; - } - - public void setResolvedAdditionalDependencies( - List resolvedAdditionalDependencies) { - this.resolvedAdditionalDependencies = resolvedAdditionalDependencies; - } - - public byte[] getClassBytes(String classname) { - return this.classBytes.get(classname); - } - - public boolean wasSuccessful() { - return this.successfulCompilation; - } - - public List> getCompiledClasses() { - return this.compiledClasses; - } - - public void setCompiledClasses(List> compiledClasses) { - this.compiledClasses = compiledClasses; - } - - public List getCompilationMessages() { - return Collections.unmodifiableList(this.compilationMessages); - } - - public void recordCompilationMessage(CompilationMessage message) { - this.compilationMessages.add(message); - } - - public void recordCompilationMessages(List messages) { - this.compilationMessages.addAll(messages); - } - - public String toString() { - StringBuilder s = new StringBuilder(); - s.append("Compilation result: #classes=" + this.compiledClasses.size() - + " #messages=" + this.compilationMessages.size() + "\n"); - s.append("Compiled classes:\n").append(this.compiledClasses).append("\n"); - s.append("Compilation messages:\n").append(this.compilationMessages).append("\n"); - return s.toString(); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompiledClassDefinition.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompiledClassDefinition.java deleted file mode 100644 index 9502f5ff9..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompiledClassDefinition.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -/** - * Encapsulates a name with the bytes for its class definition. - * - * @author Andy Clement - */ -public class CompiledClassDefinition { - - private byte[] bytes; - - private String filename; - - private String classname; - - public CompiledClassDefinition(String filename, byte[] bytes) { - this.filename = filename; - this.bytes = bytes; - this.classname = filename; - if (this.classname.startsWith("/")) { - this.classname = this.classname.substring(1); - } - this.classname = this.classname.replace('/', '.').substring(0, - this.classname.length() - 6); // strip - // off - // .class - } - - public String getName() { - return this.filename; - } - - public byte[] getBytes() { - return this.bytes; - } - - @Override - public String toString() { - return "CompiledClassDefinition(name=" + getName() + ",#bytes=" - + getBytes().length + ")"; - } - - public String getClassName() { - return this.classname; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompositeProxySelector.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompositeProxySelector.java deleted file mode 100644 index 0e3cc7f4d..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/CompositeProxySelector.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.util.ArrayList; -import java.util.List; - -import org.eclipse.aether.repository.Proxy; -import org.eclipse.aether.repository.ProxySelector; -import org.eclipse.aether.repository.RemoteRepository; - -/** - * Composite {@link ProxySelector}. - * - * @author Dave Syer - */ -public class CompositeProxySelector implements ProxySelector { - - private List selectors = new ArrayList(); - - public CompositeProxySelector(List selectors) { - this.selectors = selectors; - } - - @Override - public Proxy getProxy(RemoteRepository repository) { - for (ProxySelector selector : this.selectors) { - Proxy proxy = selector.getProxy(repository); - if (proxy != null) { - return proxy; - } - } - return null; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DependencyResolver.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DependencyResolver.java deleted file mode 100644 index a7ee52c50..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DependencyResolver.java +++ /dev/null @@ -1,485 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Properties; -import java.util.Set; - -import javax.inject.Singleton; - -import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import com.google.inject.name.Named; -import com.google.inject.name.Names; -import org.apache.maven.artifact.repository.ArtifactRepository; -import org.apache.maven.artifact.repository.ArtifactRepositoryPolicy; -import org.apache.maven.artifact.repository.MavenArtifactRepository; -import org.apache.maven.artifact.repository.layout.DefaultRepositoryLayout; -import org.apache.maven.model.Model; -import org.apache.maven.model.io.DefaultModelReader; -import org.apache.maven.model.io.ModelReader; -import org.apache.maven.model.locator.DefaultModelLocator; -import org.apache.maven.model.locator.ModelLocator; -import org.apache.maven.model.validation.DefaultModelValidator; -import org.apache.maven.model.validation.ModelValidator; -import org.apache.maven.project.DefaultProjectBuildingRequest; -import org.apache.maven.project.DependencyResolutionResult; -import org.apache.maven.project.ProjectBuilder; -import org.apache.maven.project.ProjectBuildingException; -import org.apache.maven.project.ProjectBuildingRequest; -import org.apache.maven.project.ProjectBuildingRequest.RepositoryMerging; -import org.apache.maven.project.ProjectBuildingResult; -import org.apache.maven.repository.internal.DefaultArtifactDescriptorReader; -import org.apache.maven.repository.internal.DefaultVersionRangeResolver; -import org.apache.maven.repository.internal.DefaultVersionResolver; -import org.apache.maven.repository.internal.MavenRepositorySystemUtils; -import org.apache.maven.repository.internal.SnapshotMetadataGeneratorFactory; -import org.apache.maven.repository.internal.VersionsMetadataGeneratorFactory; -import org.apache.maven.settings.Profile; -import org.apache.maven.settings.Repository; -import org.codehaus.plexus.ContainerConfiguration; -import org.codehaus.plexus.DefaultContainerConfiguration; -import org.codehaus.plexus.DefaultPlexusContainer; -import org.codehaus.plexus.MutablePlexusContainer; -import org.codehaus.plexus.PlexusConstants; -import org.codehaus.plexus.PlexusContainer; -import org.codehaus.plexus.classworlds.ClassWorld; -import org.eclipse.aether.DefaultRepositorySystemSession; -import org.eclipse.aether.RepositorySystem; -import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; -import org.eclipse.aether.graph.Dependency; -import org.eclipse.aether.impl.ArtifactDescriptorReader; -import org.eclipse.aether.impl.MetadataGeneratorFactory; -import org.eclipse.aether.impl.VersionRangeResolver; -import org.eclipse.aether.impl.VersionResolver; -import org.eclipse.aether.impl.guice.AetherModule; -import org.eclipse.aether.repository.LocalRepository; -import org.eclipse.aether.repository.NoLocalRepositoryManagerException; -import org.eclipse.aether.repository.ProxySelector; -import org.eclipse.aether.repository.RemoteRepository; -import org.eclipse.aether.repository.RepositoryPolicy; -import org.eclipse.aether.resolution.ArtifactRequest; -import org.eclipse.aether.resolution.ArtifactResult; -import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; -import org.eclipse.aether.spi.connector.transport.TransporterFactory; -import org.eclipse.aether.spi.localrepo.LocalRepositoryManagerFactory; -import org.eclipse.aether.transport.file.FileTransporterFactory; -import org.eclipse.aether.transport.http.HttpTransporterFactory; -import org.eclipse.aether.util.repository.JreProxySelector; -import org.eclipse.sisu.inject.DefaultBeanLocator; -import org.eclipse.sisu.plexus.ClassRealmManager; - -import org.springframework.core.io.Resource; -import org.springframework.util.StringUtils; - -/** - * Dependency resolver utility class. - * - * @author Andy Clement - */ -public final class DependencyResolver { - - private static DependencyResolver instance = new DependencyResolver(); - - private static Properties globals; - - private final Object lock = new Object(); - - private LocalRepositoryManagerFactory localRepositoryManagerFactory; - - private PlexusContainer container; - - private ProjectBuilder projectBuilder; - - private RepositorySystem repositorySystem; - - private MavenSettings settings; - - private DependencyResolver() { - } - - public static DependencyResolver instance() { - return instance; - } - - public static void close() { - instance = new DependencyResolver(); - } - - static Properties getGlobals() { - return globals; - } - - private void initialize() { - if (this.container == null) { - synchronized (this.lock) { - if (this.container == null) { - ClassWorld classWorld = new ClassWorld("plexus.core", - Thread.currentThread().getContextClassLoader()); - ContainerConfiguration config = new DefaultContainerConfiguration() - .setClassWorld(classWorld) - .setRealm(classWorld.getClassRealm("plexus.core")) - .setClassPathScanning(PlexusConstants.SCANNING_INDEX) - .setAutoWiring(true).setName("maven"); - PlexusContainer container; - try { - container = new DefaultPlexusContainer(config, new AetherModule(), - new DependencyResolutionModule()); - this.localRepositoryManagerFactory = container - .lookup(LocalRepositoryManagerFactory.class); - container.addComponent( - new ClassRealmManager((MutablePlexusContainer) container, - new DefaultBeanLocator()), - ClassRealmManager.class.getName()); - this.projectBuilder = container.lookup(ProjectBuilder.class); - this.repositorySystem = container.lookup(RepositorySystem.class); - } - catch (Exception e) { - throw new IllegalStateException("Cannot create container", e); - } - this.container = container; - this.settings = new MavenSettingsReader().readSettings(); - } - } - } - } - - public List dependencies(Resource resource) { - return dependencies(resource, new Properties()); - } - - public List dependencies(final Resource resource, - final Properties properties) { - initialize(); - try { - ProjectBuildingRequest request = getProjectBuildingRequest(properties); - request.setResolveDependencies(true); - synchronized (DependencyResolver.class) { - ProjectBuildingResult result = this.projectBuilder - .build(new PropertiesModelSource(properties, resource), request); - DependencyResolver.globals = null; - DependencyResolutionResult dependencies = result - .getDependencyResolutionResult(); - if (!dependencies.getUnresolvedDependencies().isEmpty()) { - StringBuilder builder = new StringBuilder(); - for (Dependency dependency : dependencies - .getUnresolvedDependencies()) { - List errors = dependencies - .getResolutionErrors(dependency); - for (Exception exception : errors) { - if (builder.length() > 0) { - builder.append("\n"); - } - builder.append(exception.getMessage()); - } - } - throw new RuntimeException(builder.toString()); - } - return runtime(dependencies.getDependencies()); - } - } - catch (ProjectBuildingException | NoLocalRepositoryManagerException e) { - throw new IllegalStateException("Cannot build model", e); - } - } - - public File resolve(Dependency dependency) { - initialize(); - return collectNonTransitive(Arrays.asList(dependency)).iterator().next() - .getArtifact().getFile(); - } - - private List runtime(List dependencies) { - List list = new ArrayList<>(); - for (Dependency dependency : dependencies) { - if (!"test".equals(dependency.getScope()) - && !"provided".equals(dependency.getScope())) { - list.add(dependency); - } - } - return list; - } - - private ProjectBuildingRequest getProjectBuildingRequest(Properties properties) - throws NoLocalRepositoryManagerException { - DefaultProjectBuildingRequest projectBuildingRequest = new DefaultProjectBuildingRequest(); - DefaultRepositorySystemSession session = createSession(properties); - projectBuildingRequest.setRepositoryMerging(RepositoryMerging.REQUEST_DOMINANT); - projectBuildingRequest.setRemoteRepositories(mavenRepositories(properties)); - projectBuildingRequest.getRemoteRepositories() - .addAll(mavenRepositories(this.settings)); - projectBuildingRequest.setRepositorySession(session); - projectBuildingRequest.setProcessPlugins(false); - projectBuildingRequest.setBuildStartTime(new Date()); - projectBuildingRequest.setUserProperties(properties); - projectBuildingRequest.setSystemProperties(System.getProperties()); - return projectBuildingRequest; - } - - private Collection mavenRepositories( - MavenSettings settings) { - List list = new ArrayList<>(); - for (Profile profile : settings.getActiveProfiles()) { - for (Repository repository : profile.getRepositories()) { - addRepositoryIfMissing(list, repository.getId(), repository.getUrl(), - repository.getReleases() != null - ? repository.getReleases().isEnabled() : true, - repository.getSnapshots() != null - ? repository.getSnapshots().isEnabled() : false); - } - } - return list; - } - - private List mavenRepositories(Properties properties) { - List list = new ArrayList<>(); - addRepositoryIfMissing(list, "spring-snapshots", - "https://repo.spring.io/libs-snapshot", true, true); - addRepositoryIfMissing(list, "central", "https://repo1.maven.org/maven2", true, - false); - return list; - } - - private List aetherRepositories(Properties properties) { - List list = new ArrayList<>(); - for (ArtifactRepository input : mavenRepositories(properties)) { - list.add(remote(input)); - } - return list; - } - - private RemoteRepository remote(ArtifactRepository input) { - return new RemoteRepository.Builder(input.getId(), input.getLayout().getId(), - input.getUrl()).setSnapshotPolicy(policy(input.getSnapshots())) - .setReleasePolicy(policy(input.getReleases())).build(); - } - - private RepositoryPolicy policy(ArtifactRepositoryPolicy input) { - RepositoryPolicy policy = new RepositoryPolicy(input.isEnabled(), - RepositoryPolicy.UPDATE_POLICY_DAILY, - RepositoryPolicy.CHECKSUM_POLICY_WARN); - return policy; - } - - private void addRepositoryIfMissing(List list, String id, - String url, boolean releases, boolean snapshots) { - for (ArtifactRepository repo : list) { - if (url.equals(repo.getUrl())) { - return; - } - if (id.equals(repo.getId())) { - return; - } - } - list.add(repo(id, url, releases, snapshots)); - } - - private ArtifactRepository repo(String id, String url, boolean releases, - boolean snapshots) { - MavenArtifactRepository repository = new MavenArtifactRepository(); - repository.setLayout(new DefaultRepositoryLayout()); - repository.setId(id); - repository.setUrl(url); - ArtifactRepositoryPolicy enabled = new ArtifactRepositoryPolicy(); - enabled.setEnabled(true); - ArtifactRepositoryPolicy disabled = new ArtifactRepositoryPolicy(); - disabled.setEnabled(false); - repository.setReleaseUpdatePolicy(releases ? enabled : disabled); - repository.setSnapshotUpdatePolicy(snapshots ? enabled : disabled); - return repository; - } - - private DefaultRepositorySystemSession createSession(Properties properties) - throws NoLocalRepositoryManagerException { - DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); - LocalRepository repository = localRepository(properties); - session.setLocalRepositoryManager( - this.localRepositoryManagerFactory.newInstance(session, repository)); - applySettings(session); - ProxySelector existing = session.getProxySelector(); - if (existing == null || !(existing instanceof CompositeProxySelector)) { - JreProxySelector fallback = new JreProxySelector(); - ProxySelector selector = existing == null ? fallback - : new CompositeProxySelector(Arrays.asList(existing, fallback)); - session.setProxySelector(selector); - } - return session; - } - - private void applySettings(DefaultRepositorySystemSession session) { - MavenSettingsReader.applySettings(this.settings, session); - } - - private LocalRepository localRepository(Properties properties) { - return new LocalRepository(getM2RepoDirectory()); - } - - public Model readModel(Resource resource) { - return readModel(resource, new Properties()); - } - - public Model readModel(final Resource resource, final Properties properties) { - initialize(); - try { - ProjectBuildingRequest request = getProjectBuildingRequest(properties); - request.setResolveDependencies(false); - ProjectBuildingResult result = this.projectBuilder - .build(new PropertiesModelSource(properties, resource), request); - return result.getProject().getModel(); - } - catch (Exception e) { - throw new IllegalStateException("Failed to build model from effective pom", - e); - } - } - - private File getM2RepoDirectory() { - return new File(getDefaultM2HomeDirectory(), "repository"); - } - - private File getDefaultM2HomeDirectory() { - String mavenRoot = System.getProperty("maven.home"); - if (StringUtils.hasLength(mavenRoot)) { - return new File(mavenRoot); - } - return new File(System.getProperty("user.home"), ".m2"); - } - - private List collectNonTransitive(List dependencies) { - try { - List artifactRequests = getArtifactRequests(dependencies); - List result = this.repositorySystem - .resolveArtifacts(createSession(new Properties()), artifactRequests); - return result; - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - - private List getArtifactRequests(List dependencies) { - List list = new ArrayList<>(); - for (Dependency dependency : dependencies) { - ArtifactRequest request = new ArtifactRequest(dependency.getArtifact(), null, - null); - request.setRepositories(aetherRepositories(new Properties())); - list.add(request); - } - return list; - } - - @SuppressWarnings("deprecation") - private static final class PropertiesModelSource - implements org.apache.maven.model.building.ModelSource { - - private final Properties properties; - - private final Resource resource; - - private PropertiesModelSource(Properties properties, Resource resource) { - this.properties = properties; - this.resource = resource; - } - - @Override - public InputStream getInputStream() throws IOException { - DependencyResolver.globals = this.properties; - return new BufferedInputStream(this.resource.getInputStream()) { - @Override - public void close() throws IOException { - DependencyResolver.globals = null; - super.close(); - } - }; - } - - @Override - public String getLocation() { - return this.resource.getDescription(); - } - - } - -} - -class DependencyResolutionModule extends AbstractModule { - - @Override - protected void configure() { - bind(ModelLocator.class).to(DefaultModelLocator.class).in(Singleton.class); - bind(ModelReader.class).to(DefaultModelReader.class).in(Singleton.class); - bind(ModelValidator.class).to(DefaultModelValidator.class).in(Singleton.class); - bind(RepositoryConnectorFactory.class).to(BasicRepositoryConnectorFactory.class) - .in(Singleton.class); - bind(ArtifactDescriptorReader.class) // - .to(DefaultArtifactDescriptorReader.class).in(Singleton.class); - bind(VersionResolver.class) // - .to(DefaultVersionResolver.class).in(Singleton.class); - bind(VersionRangeResolver.class) // - .to(DefaultVersionRangeResolver.class).in(Singleton.class); - bind(MetadataGeneratorFactory.class).annotatedWith(Names.named("snapshot")) // - .to(SnapshotMetadataGeneratorFactory.class).in(Singleton.class); - bind(MetadataGeneratorFactory.class).annotatedWith(Names.named("versions")) // - .to(VersionsMetadataGeneratorFactory.class).in(Singleton.class); - bind(TransporterFactory.class).annotatedWith(Names.named("http")) - .to(HttpTransporterFactory.class).in(Singleton.class); - bind(TransporterFactory.class).annotatedWith(Names.named("file")) - .to(FileTransporterFactory.class).in(Singleton.class); - } - - @Provides - @Singleton - Set provideMetadataGeneratorFactories( - @Named("snapshot") MetadataGeneratorFactory snapshot, - @Named("versions") MetadataGeneratorFactory versions) { - Set factories = new HashSet<>(); - factories.add(snapshot); - factories.add(versions); - return Collections.unmodifiableSet(factories); - } - - @Provides - @Singleton - Set provideRepositoryConnectorFactories( - RepositoryConnectorFactory factory) { - return Collections.singleton(factory); - } - - @Provides - @Singleton - Set provideTransporterFactories( - @Named("file") TransporterFactory file, - @Named("http") TransporterFactory http) { - // Order is decided elsewhere (by priority) - Set factories = new HashSet(); - factories.add(file); - factories.add(http); - return Collections.unmodifiableSet(factories); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEntryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEntryJavaFileObject.java deleted file mode 100644 index c4557ddff..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEntryJavaFileObject.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.Writer; -import java.net.URI; - -import javax.lang.model.element.Modifier; -import javax.lang.model.element.NestingKind; -import javax.tools.JavaFileObject; - -/** - * A JavaFileObject that represents a file in a directory. - * - * @author Andy Clement - */ -public class DirEntryJavaFileObject implements JavaFileObject { - - private File file; - - private File basedir; - - public DirEntryJavaFileObject(File basedir, File file) { - this.basedir = basedir; - this.file = file; - } - - @Override - public URI toUri() { - return this.file.toURI(); - } - - /** - * @return the path of the file relative to the base directory, for example: - * a/b/c/D.class - */ - @Override - public String getName() { - String basedirPath = this.basedir.getPath(); - String filePath = this.file.getPath(); - return filePath.substring(basedirPath.length() + 1); - } - - @Override - public InputStream openInputStream() throws IOException { - return new FileInputStream(this.file); - } - - @Override - public OutputStream openOutputStream() throws IOException { - throw new IllegalStateException("Only expected to be used for input"); - } - - @Override - public Reader openReader(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "openReader() not supported on class file: " + getName()); - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "getCharContent() not supported on class file: " + getName()); - } - - @Override - public Writer openWriter() throws IOException { - throw new IllegalStateException("only expected to be used for input"); - } - - @Override - public long getLastModified() { - return this.file.lastModified(); - } - - @Override - public boolean delete() { - return false; // This object is for read only access to a class - } - - @Override - public Kind getKind() { - return Kind.CLASS; - } - - @Override - public boolean isNameCompatible(String simpleName, Kind kind) { - if (kind != Kind.CLASS) { - return false; - } - String name = getName(); - int lastSlash = name.lastIndexOf('/'); - return name.substring(lastSlash + 1).equals(simpleName + ".class"); - } - - @Override - public NestingKind getNestingKind() { - return null; - } - - @Override - public Modifier getAccessLevel() { - return null; - } - - @Override - public int hashCode() { - return this.file.getName().hashCode() * 37 + this.basedir.getName().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof DirEntryJavaFileObject)) { - return false; - } - DirEntryJavaFileObject that = (DirEntryJavaFileObject) obj; - return (this.basedir.getName().equals(that.basedir.getName())) - && (this.file.getName().equals(that.file.getName())); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEnumeration.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEnumeration.java deleted file mode 100644 index 48017ea84..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/DirEnumeration.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.NoSuchElementException; - -/** - * Walks a directory hierarchy from some base directory discovering files. - * - * @author Andy Clement - */ -public class DirEnumeration implements Enumeration { - - // The starting point - private File basedir; - - // Candidates collected so far - private List filesToReturn; - - // Places still to explore for candidates - private List directoriesToExplore; - - public DirEnumeration(File basedir) { - this.basedir = basedir; - } - - private void computeValue() { - if (this.filesToReturn == null) { // Indicates we haven't started yet - this.filesToReturn = new ArrayList<>(); - this.directoriesToExplore = new ArrayList<>(); - visitDirectory(this.basedir); - } - if (this.filesToReturn.size() == 0) { - while (this.filesToReturn.size() == 0 - && this.directoriesToExplore.size() != 0) { - File nextDir = this.directoriesToExplore.get(0); - this.directoriesToExplore.remove(0); - visitDirectory(nextDir); - } - } - } - - @Override - public boolean hasMoreElements() { - computeValue(); - return this.filesToReturn.size() != 0; - } - - @Override - public File nextElement() { - computeValue(); - if (this.filesToReturn.size() == 0) { - throw new NoSuchElementException(); - } - File toReturn = this.filesToReturn.get(0); - this.filesToReturn.remove(0); - return toReturn; - } - - private void visitDirectory(File dir) { - File[] files = dir.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - this.directoriesToExplore.add(file); - } - else { - this.filesToReturn.add(file); - } - } - } - } - - public File getDirectory() { - return this.basedir; - } - - /** - * Return the relative path of this file to the base directory that the directory - * enumeration was started for. - * @param file a file discovered returned by this enumeration - * @return the relative path of the file (for example: a/b/c/D.class) - */ - public String getName(File file) { - String basedirPath = this.basedir.getPath(); - String filePath = file.getPath(); - if (!filePath.startsWith(basedirPath)) { - throw new IllegalStateException("The file '" + filePath - + "' is not nested below the base directory '" + basedirPath + "'"); - } - return filePath.substring(basedirPath.length() + 1); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/InMemoryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/InMemoryJavaFileObject.java deleted file mode 100644 index 5203fb374..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/InMemoryJavaFileObject.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.CharArrayWriter; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.Reader; -import java.io.Writer; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; - -import javax.lang.model.element.Modifier; -import javax.lang.model.element.NestingKind; -import javax.tools.FileObject; -import javax.tools.JavaFileManager.Location; -import javax.tools.JavaFileObject; -import javax.tools.StandardLocation; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * A JavaFileObject that represents a source artifact created for compilation or an output - * artifact producing during compilation (a .class file or some other thing if an - * annotation processor has run). In order to be clear what it is being used for there are - * static factory methods that ask for specific types of file. - * - * @author Andy Clement - */ -public final class InMemoryJavaFileObject implements JavaFileObject { - - private final static Logger logger = LoggerFactory - .getLogger(InMemoryJavaFileObject.class); - - private Location location; - - private String packageName; - - private String relativeName; - - private FileObject sibling; - - private String className; - - private Kind kind; - - private byte[] content = null; - - private long lastModifiedTime = 0; - - private URI uri = null; - - private InMemoryJavaFileObject() { - } - - public static InMemoryJavaFileObject getFileObject(Location location, - String packageName, String relativeName, FileObject sibling) { - InMemoryJavaFileObject retval = new InMemoryJavaFileObject(); - retval.kind = Kind.OTHER; - retval.location = location; - retval.packageName = packageName; - retval.relativeName = relativeName; - retval.sibling = sibling; - return retval; - } - - public static InMemoryJavaFileObject getJavaFileObject(Location location, - String className, Kind kind, FileObject sibling) { - InMemoryJavaFileObject retval = new InMemoryJavaFileObject(); - retval.location = location; - retval.className = className; - retval.kind = kind; - retval.sibling = sibling; - return retval; - } - - public static InMemoryJavaFileObject getSourceJavaFileObject(String className, - String content) { - InMemoryJavaFileObject retval = new InMemoryJavaFileObject(); - retval.location = StandardLocation.SOURCE_PATH; - retval.className = className; - retval.kind = Kind.SOURCE; - retval.content = content.getBytes(); - return retval; - } - - public byte[] getBytes() { - return this.content; - } - - public String toString() { - return "OutputJavaFileObject: Location=" + this.location + ",className=" - + this.className + ",kind=" + this.kind + ",relativeName=" - + this.relativeName + ",sibling=" + this.sibling + ",packageName=" - + this.packageName; - } - - @Override - public URI toUri() { - // These memory based output files 'pretend' to be relative to the file system - // root - if (this.uri == null) { - String name = null; - if (this.className != null) { - name = this.className.replace('.', '/'); - } - else if (this.packageName != null && this.packageName.length() != 0) { - name = this.packageName.replace('.', '/') + '/' + this.relativeName; - } - else { - name = this.relativeName; - } - - String uriString = null; - try { - uriString = "file:/" + name + this.kind.extension; - this.uri = new URI(uriString); - } - catch (URISyntaxException e) { - throw new IllegalStateException( - "Unexpected URISyntaxException for string '" + uriString + "'", - e); - } - } - return this.uri; - } - - @Override - public String getName() { - return toUri().getPath(); - } - - @Override - public InputStream openInputStream() throws IOException { - if (this.content == null) { - throw new FileNotFoundException(); - } - logger.debug("opening input stream for {}", getName()); - return new ByteArrayInputStream(this.content); - } - - @Override - public OutputStream openOutputStream() throws IOException { - logger.debug("opening output stream for {}", getName()); - return new ByteArrayOutputStream() { - @Override - public void close() throws IOException { - super.close(); - InMemoryJavaFileObject.this.lastModifiedTime = System.currentTimeMillis(); - InMemoryJavaFileObject.this.content = this.toByteArray(); - } - }; - } - - @Override - public Reader openReader(boolean ignoreEncodingErrors) throws IOException { - return new InputStreamReader(openInputStream(), Charset.defaultCharset()); - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - if (this.kind != Kind.SOURCE) { - throw new UnsupportedOperationException( - "getCharContent() not supported on file object: " + getName()); - } - // Not yet supporting encodings - return (this.content == null ? null : new String(this.content)); - } - - @Override - public Writer openWriter() throws IOException { - // Let's not enforce this restriction right now - // if (kind == Kind.CLASS) { - // throw new UnsupportedOperationException("openWriter() not supported on file - // object: " + getName()); - // } - return new CharArrayWriter() { - @Override - public void close() { - InMemoryJavaFileObject.this.lastModifiedTime = System.currentTimeMillis(); - InMemoryJavaFileObject.this.content = new String(toCharArray()) - .getBytes(); // Ignoring encoding... - } - }; - } - - @Override - public long getLastModified() { - return this.lastModifiedTime; - } - - @Override - public boolean delete() { - return false; - } - - @Override - public Kind getKind() { - return this.kind; - } - - public boolean isNameCompatible(String simpleName, Kind kind) { - String baseName = simpleName + kind.extension; - return kind.equals(getKind()) && (baseName.equals(toUri().getPath()) - || toUri().getPath().endsWith("/" + baseName)); - } - - @Override - public NestingKind getNestingKind() { - return null; - } - - @Override - public Modifier getAccessLevel() { - return null; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableClasspath.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableClasspath.java deleted file mode 100644 index b10e7e0fa..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableClasspath.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Stack; -import java.util.StringTokenizer; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; - -import javax.tools.JavaFileObject; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager.CompilationInfoCache; -import org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager.CompilationInfoCache.ArchiveInfo; - -/** - * Iterable that will produce an iterator that returns classes found on a specified - * classpath that meet specified criteria. For jars it finds, the iterator will go into - * nested jars - this handles the situation with a spring boot uberjar. - * - * @author Andy Clement - */ -public class IterableClasspath extends CloseableFilterableJavaFileObjectIterable { - - private static Logger logger = LoggerFactory.getLogger(IterableClasspath.class); - - private List classpathEntries = new ArrayList<>(); - - private List openArchives = new ArrayList<>(); - - /** - * @param compilationInfoCache cache of info that may help accelerate compilation - * @param classpath a classpath of jars/directories - * @param packageNameFilter an optional package name if choosing to filter (e.g. - * com.example) - * @param includeSubpackages if true, include results in subpackages of the specified - * package filter - */ - IterableClasspath(CompilationInfoCache compilationInfoCache, String classpath, - String packageNameFilter, boolean includeSubpackages) { - super(compilationInfoCache, packageNameFilter, includeSubpackages); - StringTokenizer tokenizer = new StringTokenizer(classpath, File.pathSeparator); - while (tokenizer.hasMoreElements()) { - String nextEntry = tokenizer.nextToken(); - File f = new File(nextEntry); - if (f.exists()) { - // Skip iterating over archives that cannot possibly match the filter - if (this.packageNameFilter != null - && this.packageNameFilter.length() > 0) { - ArchiveInfo archiveInfo = compilationInfoCache.getArchiveInfoFor(f); - if (archiveInfo != null - && !archiveInfo.containsPackage(this.packageNameFilter, - this.includeSubpackages)) { - continue; - } - } - this.classpathEntries.add(f); - } - else { - logger.debug("path element does not exist {}", f); - } - } - } - - public void close() { - for (ZipFile openArchive : this.openArchives) { - try { - openArchive.close(); - } - catch (IOException ioe) { - logger.debug("Unexpected error closing archive {}", openArchive, ioe); - } - } - this.openArchives.clear(); - } - - public Iterator iterator() { - return new ClasspathEntriesIterator(); - } - - public void reset() { - close(); - } - - static class ZipEnumerator implements Enumeration { - - private ZipInputStream zis; - - private ZipEntry nextEntry = null; - - ZipEnumerator(ZipInputStream zis) { - this.zis = zis; - } - - @Override - public boolean hasMoreElements() { - try { - this.nextEntry = this.zis.getNextEntry(); - } - catch (IOException ioe) { - this.nextEntry = null; - } - return this.nextEntry != null; - } - - @Override - public ZipEntry nextElement() { - ZipEntry retval = this.nextEntry; - this.nextEntry = null; - return retval; - } - - } - - class ClasspathEntriesIterator implements Iterator { - - private int currentClasspathEntriesIndex = 0; - - // Walking one of three possible things: directory tree, zip, or Java runtime - // packaged in JDK9+ form - private File openDirectory = null; - - private DirEnumeration openDirectoryEnumeration = null; - - private ZipFile openArchive = null; - - private File openFile = null; - - private ZipEntry nestedZip = null; - - private Stack> openArchiveEnumeration = null; - - private File openJrt; - - private JrtFsEnumeration openJrtEnumeration = null; - - private JavaFileObject nextEntry = null; - - private void findNext() { - if (this.nextEntry == null) { - try { - while (this.openArchive != null || this.openDirectory != null - || this.openJrt != null - || this.currentClasspathEntriesIndex < IterableClasspath.this.classpathEntries - .size()) { - if (this.openArchive == null && this.openDirectory == null - && this.openJrt == null) { - // Open the next item - File nextFile = IterableClasspath.this.classpathEntries - .get(this.currentClasspathEntriesIndex); - if (nextFile.isDirectory()) { - this.openDirectory = nextFile; - this.openDirectoryEnumeration = new DirEnumeration( - nextFile); - } - else if (nextFile.getName().endsWith("jrt-fs.jar")) { - this.openJrt = nextFile; - this.openJrtEnumeration = new JrtFsEnumeration(nextFile, - null); - } - else { - this.openFile = nextFile; - this.openArchive = new ZipFile(nextFile); - IterableClasspath.this.openArchives.add(this.openArchive); - this.openArchiveEnumeration = new Stack>(); - this.openArchiveEnumeration - .push(this.openArchive.entries()); - } - this.currentClasspathEntriesIndex++; - } - if (this.openArchiveEnumeration != null) { - while (!this.openArchiveEnumeration.isEmpty()) { - while (this.openArchiveEnumeration.peek() - .hasMoreElements()) { - ZipEntry entry = this.openArchiveEnumeration.peek() - .nextElement(); - String entryName = entry.getName(); - if (accept(entryName)) { - if (this.nestedZip != null) { - this.nextEntry = new NestedZipEntryJavaFileObject( - this.openFile, this.openArchive, - this.nestedZip, entry); - } - else { - this.nextEntry = new ZipEntryJavaFileObject( - this.openFile, this.openArchive, - entry); - } - return; - } - else if (this.nestedZip == null - && entryName.startsWith( - MemoryBasedJavaFileManager.BOOT_PACKAGING_PREFIX_FOR_LIBRARIES) - && entryName.endsWith(".jar")) { - // nested jar in uber jar - logger.debug("opening nested archive {}", - entry.getName()); - ZipInputStream zis = new ZipInputStream( - this.openArchive.getInputStream(entry)); - Enumeration nestedZipEnumerator = new ZipEnumerator( - zis); - this.nestedZip = entry; - this.openArchiveEnumeration - .push(nestedZipEnumerator); - } - } - this.openArchiveEnumeration.pop(); - if (this.nestedZip == null) { - this.openArchive = null; - this.openFile = null; - } - else { - this.nestedZip = null; - } - } - this.openArchiveEnumeration = null; - this.openArchive = null; - this.openFile = null; - } - else if (this.openDirectoryEnumeration != null) { - while (this.openDirectoryEnumeration.hasMoreElements()) { - File entry = this.openDirectoryEnumeration.nextElement(); - String name = this.openDirectoryEnumeration - .getName(entry); - if (accept(name)) { - this.nextEntry = new DirEntryJavaFileObject( - this.openDirectoryEnumeration.getDirectory(), - entry); - return; - } - } - this.openDirectoryEnumeration = null; - this.openDirectory = null; - } - else if (this.openJrtEnumeration != null) { - while (this.openJrtEnumeration.hasMoreElements()) { - JrtEntryJavaFileObject jrtEntry = this.openJrtEnumeration - .nextElement(); - String name = this.openJrtEnumeration.getName(jrtEntry); - if (accept(name)) { - this.nextEntry = jrtEntry; - return; - } - } - this.openJrtEnumeration = null; - this.openJrt = null; - } - } - } - catch (IOException ioe) { - logger.debug("Unexpected error whilst processing classpath entries", - ioe); - } - } - } - - public boolean hasNext() { - findNext(); - return this.nextEntry != null; - } - - public JavaFileObject next() { - findNext(); - if (this.nextEntry == null) { - throw new NoSuchElementException(); - } - JavaFileObject retval = this.nextEntry; - this.nextEntry = null; - return retval; - } - - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableJrtModule.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableJrtModule.java deleted file mode 100644 index 50d5c9266..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/IterableJrtModule.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.nio.file.Path; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; - -import javax.tools.JavaFileObject; - -import org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager.CompilationInfoCache; - -/** - * Iterable that will produce an iterator that returns classes found in a specific module - * tree within the Java runtime image that exists in Java 9 and later. - * - * @author Andy Clement - */ -public class IterableJrtModule extends CloseableFilterableJavaFileObjectIterable { - - // private static Logger logger = LoggerFactory.getLogger(IterableJrtModule.class); - - Map walkers = new HashMap<>(); - - private Path moduleRootPath; - - /** - * @param compilationInfoCache cache of info that may help accelerate compilation - * @param moduleRootPath path to the base of the relevant module within the JRT image - * @param packageNameFilter an optional package name if choosing to filter (e.g. - * com.example) - * @param includeSubpackages if true, include results in subpackages of the specified - * package filter - */ - public IterableJrtModule(CompilationInfoCache compilationInfoCache, - Path moduleRootPath, String packageNameFilter, boolean includeSubpackages) { - super(compilationInfoCache, packageNameFilter, includeSubpackages); - this.moduleRootPath = moduleRootPath; - } - - public Iterator iterator() { - JrtFsEnumeration jrtFsWalker = this.walkers.get(this.moduleRootPath.toString()); - if (jrtFsWalker == null) { - jrtFsWalker = new JrtFsEnumeration(null, this.moduleRootPath); - this.walkers.put(this.moduleRootPath.toString(), jrtFsWalker); - } - jrtFsWalker.reset(); - return new IteratorOverJrtFsEnumeration(jrtFsWalker); - } - - public void close() { - } - - public void reset() { - close(); - } - - class IteratorOverJrtFsEnumeration implements Iterator { - - private JavaFileObject nextEntry = null; - - private JrtFsEnumeration jrtEnumeration; - - IteratorOverJrtFsEnumeration(JrtFsEnumeration jrtFsWalker) { - this.jrtEnumeration = jrtFsWalker; - } - - private void findNext() { - if (this.nextEntry == null) { - while (this.jrtEnumeration.hasMoreElements()) { - JrtEntryJavaFileObject jrtEntry = this.jrtEnumeration.nextElement(); - String name = this.jrtEnumeration.getName(jrtEntry); - if (accept(name)) { - this.nextEntry = jrtEntry; - return; - } - } - } - } - - public boolean hasNext() { - findNext(); - return this.nextEntry != null; - } - - public JavaFileObject next() { - findNext(); - if (this.nextEntry == null) { - throw new NoSuchElementException(); - } - JavaFileObject retval = this.nextEntry; - this.nextEntry = null; - return retval; - } - - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/JrtEntryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/JrtEntryJavaFileObject.java deleted file mode 100644 index 385d0577d..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/JrtEntryJavaFileObject.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.Writer; -import java.net.URI; -import java.nio.file.Files; -import java.nio.file.Path; - -import javax.lang.model.element.Modifier; -import javax.lang.model.element.NestingKind; -import javax.tools.JavaFileObject; - -/** - * A JavaFileObject that represents a class from the Java runtime as packaged in Java 9 - * and later. - * - * @author Andy Clement - */ -public class JrtEntryJavaFileObject implements JavaFileObject { - - private String pathToClassString; - - private Path path; - - /** - * @param path entry in the Java runtime filesystem, for example - * '/modules/java.base/java/lang/Object.class' - */ - public JrtEntryJavaFileObject(Path path) { - this.pathToClassString = path.subpath(2, path.getNameCount()).toString(); // e.g. - // java/lang/Object.class - this.path = path; - } - - @Override - public URI toUri() { - return this.path.toUri(); - } - - /** - * @return the path of the file relative to the base directory, for example: - * a/b/c/D.class - */ - @Override - public String getName() { - return this.pathToClassString; - } - - @Override - public InputStream openInputStream() throws IOException { - byte[] bytes = Files.readAllBytes(this.path); - return new ByteArrayInputStream(bytes); - } - - @Override - public OutputStream openOutputStream() throws IOException { - throw new IllegalStateException("Only expected to be used for input"); - } - - @Override - public Reader openReader(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "openReader() not supported on class file: " + getName()); - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "getCharContent() not supported on class file: " + getName()); - } - - @Override - public Writer openWriter() throws IOException { - throw new IllegalStateException("only expected to be used for input"); - } - - @Override - public long getLastModified() { - try { - return Files.getLastModifiedTime(this.path).toMillis(); - } - catch (IOException ioe) { - throw new RuntimeException( - "Unable to determine last modified time of " + this.pathToClassString, - ioe); - } - } - - @Override - public boolean delete() { - return false; // This object is for read only access to a class - } - - @Override - public Kind getKind() { - return Kind.CLASS; - } - - @Override - public boolean isNameCompatible(String simpleName, Kind kind) { - if (kind != Kind.CLASS) { - return false; - } - String name = getName(); - int lastSlash = name.lastIndexOf('/'); - return name.substring(lastSlash + 1).equals(simpleName + ".class"); - } - - @Override - public NestingKind getNestingKind() { - return null; - } - - @Override - public Modifier getAccessLevel() { - return null; - } - - @Override - public int hashCode() { - return getName().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof JrtEntryJavaFileObject)) { - return false; - } - JrtEntryJavaFileObject that = (JrtEntryJavaFileObject) obj; - return (getName().equals(that.getName())); - } - - public String getPathToClassString() { - return this.pathToClassString; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/JrtFsEnumeration.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/JrtFsEnumeration.java deleted file mode 100644 index ed9b1eb93..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/JrtFsEnumeration.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.List; -import java.util.NoSuchElementException; - -/** - * Walks a JrtFS treating it like a directory (to avoid overcomplicating the walking logic - * in IterableClasspath). - * - * @author Andy Clement - */ -public class JrtFsEnumeration implements Enumeration { - - // private final static Logger logger = - // LoggerFactory.getLogger(JrtFsEnumeration.class); - - private static URI JRT_URI = URI.create("jrt:/"); //$NON-NLS-1$ - - private final static FileSystem fs = FileSystems.getFileSystem(JRT_URI); - - private Path pathWithinJrt; - - private List jfos = new ArrayList<>(); - - private Integer counter = 0; - - private Boolean initialized = false; - - public JrtFsEnumeration(File jrtFsFile, Path pathWithinJrt) { - this.pathWithinJrt = pathWithinJrt; - ensureInitialized(); - } - - private void ensureInitialized() { - synchronized (this.initialized) { - if (this.initialized) { - return; - } - FileCacheBuilderVisitor visitor = new FileCacheBuilderVisitor(); - if (this.pathWithinJrt != null) { - try { - Files.walkFileTree(this.pathWithinJrt, visitor); - // System.out.println("JrtFs enumeration for '"+pathWithinJrt+"' with - // #"+jfos.size()+" entries"); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - else { - Iterable roots = fs.getRootDirectories(); - try { - for (java.nio.file.Path path : roots) { - Files.walkFileTree(path, visitor); - } - // System.out.println("JrtFs enumeration initialized with - // #"+jfos.size()+" entries"); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - this.initialized = true; - } - } - - @Override - public boolean hasMoreElements() { - return this.counter < this.jfos.size(); - } - - @Override - public JrtEntryJavaFileObject nextElement() { - if (this.counter >= this.jfos.size()) { - throw new NoSuchElementException(); - } - JrtEntryJavaFileObject toReturn = this.jfos.get(this.counter++); - return toReturn; - } - - /** - * Return the relative path of this file to the base directory that the directory - * enumeration was started for. - * @param file a file discovered returned by this enumeration - * @return the relative path of the file (for example: a/b/c/D.class) - */ - public String getName(JrtEntryJavaFileObject file) { - return file.getPathToClassString(); - } - - public void reset() { - this.counter = 0; - } - - class FileCacheBuilderVisitor extends SimpleFileVisitor { - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - int fnc = file.getNameCount(); - if (fnc >= 3 && file.toString().endsWith(".class")) { // There is a preceeding - // module name - e.g. - // /modules/java.base/java/lang/Object.class - // file.subpath(2, fnc); // e.g. java/lang/Object.class - JrtFsEnumeration.this.jfos.add(new JrtEntryJavaFileObject(file)); - } - return FileVisitResult.CONTINUE; - } - - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettings.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettings.java deleted file mode 100644 index da210daf4..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettings.java +++ /dev/null @@ -1,325 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.BufferedReader; -import java.io.File; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringReader; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.apache.maven.model.ActivationFile; -import org.apache.maven.model.ActivationOS; -import org.apache.maven.model.ActivationProperty; -import org.apache.maven.model.building.ModelProblemCollector; -import org.apache.maven.model.building.ModelProblemCollectorRequest; -import org.apache.maven.model.path.DefaultPathTranslator; -import org.apache.maven.model.profile.DefaultProfileSelector; -import org.apache.maven.model.profile.ProfileActivationContext; -import org.apache.maven.model.profile.activation.FileProfileActivator; -import org.apache.maven.model.profile.activation.JdkVersionProfileActivator; -import org.apache.maven.model.profile.activation.OperatingSystemProfileActivator; -import org.apache.maven.model.profile.activation.PropertyProfileActivator; -import org.apache.maven.settings.Activation; -import org.apache.maven.settings.Mirror; -import org.apache.maven.settings.Profile; -import org.apache.maven.settings.Proxy; -import org.apache.maven.settings.Server; -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.crypto.SettingsDecryptionResult; -import org.eclipse.aether.repository.Authentication; -import org.eclipse.aether.repository.AuthenticationSelector; -import org.eclipse.aether.repository.MirrorSelector; -import org.eclipse.aether.repository.ProxySelector; -import org.eclipse.aether.util.repository.AuthenticationBuilder; -import org.eclipse.aether.util.repository.ConservativeAuthenticationSelector; -import org.eclipse.aether.util.repository.DefaultAuthenticationSelector; -import org.eclipse.aether.util.repository.DefaultMirrorSelector; -import org.eclipse.aether.util.repository.DefaultProxySelector; - -/** - * An encapsulation of settings read from a user's Maven settings.xml. - * - * @author Andy Wilkinson - * @see MavenSettingsReader - */ -public class MavenSettings { - - private final boolean offline; - - private final MirrorSelector mirrorSelector; - - private final AuthenticationSelector authenticationSelector; - - private final ProxySelector proxySelector; - - private final String localRepository; - - private final List activeProfiles; - - /** - * Create a new {@link MavenSettings} instance. - * @param settings the source settings - * @param decryptedSettings the decrypted settings - */ - public MavenSettings(Settings settings, SettingsDecryptionResult decryptedSettings) { - this.offline = settings.isOffline(); - this.mirrorSelector = createMirrorSelector(settings); - this.authenticationSelector = createAuthenticationSelector(decryptedSettings); - this.proxySelector = createProxySelector(decryptedSettings); - this.localRepository = settings.getLocalRepository(); - this.activeProfiles = determineActiveProfiles(settings); - } - - private MirrorSelector createMirrorSelector(Settings settings) { - DefaultMirrorSelector selector = new DefaultMirrorSelector(); - for (Mirror mirror : settings.getMirrors()) { - selector.add(mirror.getId(), mirror.getUrl(), mirror.getLayout(), false, - mirror.getMirrorOf(), mirror.getMirrorOfLayouts()); - } - return selector; - } - - private AuthenticationSelector createAuthenticationSelector( - SettingsDecryptionResult decryptedSettings) { - DefaultAuthenticationSelector selector = new DefaultAuthenticationSelector(); - for (Server server : decryptedSettings.getServers()) { - AuthenticationBuilder auth = new AuthenticationBuilder(); - auth.addUsername(server.getUsername()).addPassword(server.getPassword()); - auth.addPrivateKey(server.getPrivateKey(), server.getPassphrase()); - selector.add(server.getId(), auth.build()); - } - return new ConservativeAuthenticationSelector(selector); - } - - private ProxySelector createProxySelector( - SettingsDecryptionResult decryptedSettings) { - DefaultProxySelector selector = new DefaultProxySelector(); - for (Proxy proxy : decryptedSettings.getProxies()) { - Authentication authentication = new AuthenticationBuilder() - .addUsername(proxy.getUsername()).addPassword(proxy.getPassword()) - .build(); - selector.add( - new org.eclipse.aether.repository.Proxy(proxy.getProtocol(), - proxy.getHost(), proxy.getPort(), authentication), - proxy.getNonProxyHosts()); - } - return selector; - } - - private List determineActiveProfiles(Settings settings) { - SpringBootCliModelProblemCollector problemCollector = new SpringBootCliModelProblemCollector(); - List activeModelProfiles = createProfileSelector() - .getActiveProfiles(createModelProfiles(settings.getProfiles()), - new SpringBootCliProfileActivationContext( - settings.getActiveProfiles()), - problemCollector); - if (!problemCollector.getProblems().isEmpty()) { - throw new IllegalStateException(createFailureMessage(problemCollector)); - } - List activeProfiles = new ArrayList(); - Map profiles = settings.getProfilesAsMap(); - for (org.apache.maven.model.Profile modelProfile : activeModelProfiles) { - activeProfiles.add(profiles.get(modelProfile.getId())); - } - return activeProfiles; - } - - private String createFailureMessage( - SpringBootCliModelProblemCollector problemCollector) { - StringWriter message = new StringWriter(); - PrintWriter printer = new PrintWriter(message); - printer.println("Failed to determine active profiles:"); - for (ModelProblemCollectorRequest problem : problemCollector.getProblems()) { - printer.println(" " + problem.getMessage() + (problem.getLocation() != null - ? " at " + problem.getLocation() : "")); - if (problem.getException() != null) { - printer.println(indentStackTrace(problem.getException(), " ")); - } - } - return message.toString(); - } - - private String indentStackTrace(Exception ex, String indent) { - return indentLines(printStackTrace(ex), indent); - } - - private String printStackTrace(Exception ex) { - StringWriter stackTrace = new StringWriter(); - PrintWriter printer = new PrintWriter(stackTrace); - ex.printStackTrace(printer); - return stackTrace.toString(); - } - - private String indentLines(String input, String indent) { - StringWriter indented = new StringWriter(); - PrintWriter writer = new PrintWriter(indented); - String line; - BufferedReader reader = new BufferedReader(new StringReader(input)); - try { - while ((line = reader.readLine()) != null) { - writer.println(indent + line); - } - } - catch (IOException ex) { - return input; - } - return indented.toString(); - } - - private DefaultProfileSelector createProfileSelector() { - DefaultProfileSelector selector = new DefaultProfileSelector(); - - selector.addProfileActivator(new FileProfileActivator() - .setPathTranslator(new DefaultPathTranslator())); - selector.addProfileActivator(new JdkVersionProfileActivator()); - selector.addProfileActivator(new PropertyProfileActivator()); - selector.addProfileActivator(new OperatingSystemProfileActivator()); - return selector; - } - - private List createModelProfiles( - List profiles) { - List modelProfiles = new ArrayList(); - for (Profile profile : profiles) { - org.apache.maven.model.Profile modelProfile = new org.apache.maven.model.Profile(); - modelProfile.setId(profile.getId()); - if (profile.getActivation() != null) { - modelProfile - .setActivation(createModelActivation(profile.getActivation())); - } - modelProfiles.add(modelProfile); - } - return modelProfiles; - } - - private org.apache.maven.model.Activation createModelActivation( - Activation activation) { - org.apache.maven.model.Activation modelActivation = new org.apache.maven.model.Activation(); - modelActivation.setActiveByDefault(activation.isActiveByDefault()); - if (activation.getFile() != null) { - ActivationFile activationFile = new ActivationFile(); - activationFile.setExists(activation.getFile().getExists()); - activationFile.setMissing(activation.getFile().getMissing()); - modelActivation.setFile(activationFile); - } - modelActivation.setJdk(activation.getJdk()); - if (activation.getOs() != null) { - ActivationOS os = new ActivationOS(); - os.setArch(activation.getOs().getArch()); - os.setFamily(activation.getOs().getFamily()); - os.setName(activation.getOs().getName()); - os.setVersion(activation.getOs().getVersion()); - modelActivation.setOs(os); - } - if (activation.getProperty() != null) { - ActivationProperty property = new ActivationProperty(); - property.setName(activation.getProperty().getName()); - property.setValue(activation.getProperty().getValue()); - modelActivation.setProperty(property); - } - return modelActivation; - } - - public boolean getOffline() { - return this.offline; - } - - public MirrorSelector getMirrorSelector() { - return this.mirrorSelector; - } - - public AuthenticationSelector getAuthenticationSelector() { - return this.authenticationSelector; - } - - public ProxySelector getProxySelector() { - return this.proxySelector; - } - - public String getLocalRepository() { - return this.localRepository; - } - - public List getActiveProfiles() { - return this.activeProfiles; - } - - private static final class SpringBootCliProfileActivationContext - implements ProfileActivationContext { - - private final List activeProfiles; - - SpringBootCliProfileActivationContext(List activeProfiles) { - this.activeProfiles = activeProfiles; - } - - @Override - public List getActiveProfileIds() { - return this.activeProfiles; - } - - @Override - public List getInactiveProfileIds() { - return Collections.emptyList(); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Override - public Map getSystemProperties() { - return (Map) System.getProperties(); - } - - @Override - public Map getUserProperties() { - return Collections.emptyMap(); - } - - @Override - public File getProjectDirectory() { - return new File("."); - } - - @Override - public Map getProjectProperties() { - return Collections.emptyMap(); - } - - } - - private static final class SpringBootCliModelProblemCollector - implements ModelProblemCollector { - - private final List problems = new ArrayList(); - - @Override - public void add(ModelProblemCollectorRequest req) { - this.problems.add(req); - } - - List getProblems() { - return this.problems; - } - - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettingsReader.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettingsReader.java deleted file mode 100644 index a2292f982..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MavenSettingsReader.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.lang.reflect.Field; - -import org.apache.maven.settings.Settings; -import org.apache.maven.settings.building.DefaultSettingsBuilderFactory; -import org.apache.maven.settings.building.DefaultSettingsBuildingRequest; -import org.apache.maven.settings.building.SettingsBuildingException; -import org.apache.maven.settings.building.SettingsBuildingRequest; -import org.apache.maven.settings.crypto.DefaultSettingsDecrypter; -import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; -import org.apache.maven.settings.crypto.SettingsDecrypter; -import org.apache.maven.settings.crypto.SettingsDecryptionResult; -import org.eclipse.aether.DefaultRepositorySystemSession; -import org.eclipse.aether.internal.impl.SimpleLocalRepositoryManagerFactory; -import org.eclipse.aether.repository.LocalRepository; -import org.eclipse.aether.repository.NoLocalRepositoryManagerException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonatype.plexus.components.cipher.DefaultPlexusCipher; -import org.sonatype.plexus.components.cipher.PlexusCipherException; -import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher; - -/** - * {@code MavenSettingsReader} reads settings from a user's Maven settings.xml file, - * decrypting them if necessary using settings-security.xml. - * - * @author Andy Wilkinson - */ -public class MavenSettingsReader { - - private static final Logger log = LoggerFactory.getLogger(MavenSettingsReader.class); - - private final String homeDir; - - public MavenSettingsReader() { - this(System.getProperty("user.home")); - } - - public MavenSettingsReader(String homeDir) { - this.homeDir = homeDir; - } - - public static void applySettings(MavenSettings settings, - DefaultRepositorySystemSession session) { - if (settings.getLocalRepository() != null) { - try { - session.setLocalRepositoryManager( - new SimpleLocalRepositoryManagerFactory().newInstance(session, - new LocalRepository(settings.getLocalRepository()))); - } - catch (NoLocalRepositoryManagerException e) { - throw new IllegalStateException( - "Cannot set local repository to " + settings.getLocalRepository(), - e); - } - } - session.setOffline(settings.getOffline()); - session.setMirrorSelector(settings.getMirrorSelector()); - session.setAuthenticationSelector(settings.getAuthenticationSelector()); - session.setProxySelector(settings.getProxySelector()); - } - - public MavenSettings readSettings() { - Settings settings = loadSettings(); - SettingsDecryptionResult decrypted = decryptSettings(settings); - if (!decrypted.getProblems().isEmpty()) { - log.error( - "Maven settings decryption failed. Some Maven repositories may be inaccessible"); - // Continue - the encrypted credentials may not be used - } - return new MavenSettings(settings, decrypted); - } - - private Settings loadSettings() { - File settingsFile = new File(this.homeDir, ".m2/settings.xml"); - if (settingsFile.exists()) { - log.info("Reading settings from: " + settingsFile); - } - else { - log.info("No settings found at: " + settingsFile); - } - SettingsBuildingRequest request = new DefaultSettingsBuildingRequest(); - request.setUserSettingsFile(settingsFile); - request.setSystemProperties(System.getProperties()); - try { - return new DefaultSettingsBuilderFactory().newInstance().build(request) - .getEffectiveSettings(); - } - catch (SettingsBuildingException ex) { - throw new IllegalStateException( - "Failed to build settings from " + settingsFile, ex); - } - } - - private SettingsDecryptionResult decryptSettings(Settings settings) { - DefaultSettingsDecryptionRequest request = new DefaultSettingsDecryptionRequest( - settings); - - return createSettingsDecrypter().decrypt(request); - } - - private SettingsDecrypter createSettingsDecrypter() { - SettingsDecrypter settingsDecrypter = new DefaultSettingsDecrypter(); - setField(DefaultSettingsDecrypter.class, "securityDispatcher", settingsDecrypter, - new SpringBootSecDispatcher()); - return settingsDecrypter; - } - - private void setField(Class sourceClass, String fieldName, Object target, - Object value) { - try { - Field field = sourceClass.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } - catch (Exception ex) { - throw new IllegalStateException( - "Failed to set field '" + fieldName + "' on '" + target + "'", ex); - } - } - - private class SpringBootSecDispatcher extends DefaultSecDispatcher { - - private static final String SECURITY_XML = ".m2/settings-security.xml"; - - SpringBootSecDispatcher() { - File file = new File(MavenSettingsReader.this.homeDir, SECURITY_XML); - this._configurationFile = file.getAbsolutePath(); - try { - this._cipher = new DefaultPlexusCipher(); - } - catch (PlexusCipherException e) { - throw new IllegalStateException(e); - } - } - - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java deleted file mode 100644 index 2b20695cf..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/MemoryBasedJavaFileManager.java +++ /dev/null @@ -1,769 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.io.IOException; -import java.net.URI; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.StringTokenizer; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; - -import javax.tools.FileObject; -import javax.tools.JavaFileManager; -import javax.tools.JavaFileObject; -import javax.tools.JavaFileObject.Kind; -import javax.tools.StandardLocation; - -import org.eclipse.aether.artifact.DefaultArtifact; -import org.eclipse.aether.graph.Dependency; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.springframework.cloud.function.compiler.java.IterableClasspath.ZipEnumerator; - -/** - * A file manager that serves source code from in memory and ensures output results are - * kept in memory rather than being flushed out to disk. The JavaFileManager is also used - * as a lookup mechanism for resolving types. - * - * @author Andy Clement - * @author Oleg Zhurakousky - */ -public class MemoryBasedJavaFileManager implements JavaFileManager { - - final static String BOOT_PACKAGING_PREFIX_FOR_CLASSES = "BOOT-INF/classes/"; - - final static String BOOT_PACKAGING_PREFIX_FOR_LIBRARIES = "BOOT-INF/lib/"; - - private static Logger logger = LoggerFactory - .getLogger(MemoryBasedJavaFileManager.class); - - private static URI JRT_URI = URI.create("jrt:/"); - - private static FileSystem fs; - - private CompilationOutputCollector outputCollector; - - private Map resolvedAdditionalDependencies = new LinkedHashMap<>(); - - private String platformClasspath; - - private String classpath; - - private CompilationInfoCache compilationInfoCache; - - private Map iterables = new HashMap<>(); - - private String jrtFsFilePath = null; - - private boolean checkedForJrtFsPath = false; - - public MemoryBasedJavaFileManager() { - this.outputCollector = new CompilationOutputCollector(); - this.compilationInfoCache = new CompilationInfoCache(); - } - - private static FileSystem getJrtFs() { - if (fs == null) { - fs = FileSystems.getFileSystem(JRT_URI); - } - return fs; - } - - @Override - public int isSupportedOption(String option) { - logger.debug("isSupportedOption({})", option); - return -1; // Not yet supporting options - } - - @Override - public ClassLoader getClassLoader(Location location) { - // Do not simply return the context classloader as it may get closed and then - // be unusable for loading any further classes - logger.debug("getClassLoader({})", location); - return null; // Do not currently need to load plugins - } - - private String getPlatformClassPath() { - if (this.platformClasspath == null) { - this.platformClasspath = System.getProperty("sun.boot.class.path"); - } - if (this.platformClasspath == null) { - this.platformClasspath = ""; - } - return this.platformClasspath; - } - - @Override - public Iterable list(Location location, String packageName, - Set kinds, boolean recurse) throws IOException { - logger.debug("list({},{},{},{})", location, packageName, kinds, recurse); - String classpath = ""; - Path moduleRootPath = null; - if (location instanceof JDKModuleLocation - && (kinds == null || kinds.contains(Kind.CLASS))) { - // list(org.springframework.cloud.function.compiler.java.MemoryBasedJavaFileManager$JDKModuleLocation@550a1967, - // java.lang,[SOURCE, CLASS, HTML, OTHER],false) - moduleRootPath = ((JDKModuleLocation) location).getModuleRootPath(); - logger.debug("For JDKModuleLocation " + location.toString() + " root path is " - + moduleRootPath); - } - else if (location == StandardLocation.PLATFORM_CLASS_PATH - && (kinds == null || kinds.contains(Kind.CLASS))) { - classpath = getPlatformClassPath(); - // if (classpath.length() == 0) { - // if (hasJrtFsPath()) { - // classpath = getJrtFsPath(); - // } - // } - logger.debug("Creating iterable for boot class path: {}", classpath); - } - else if (location == StandardLocation.CLASS_PATH - && (kinds == null || kinds.contains(Kind.CLASS))) { - String javaClassPath = getClassPath(); - if (!this.resolvedAdditionalDependencies.isEmpty()) { - for (File resolvedAdditionalDependency : this.resolvedAdditionalDependencies - .values()) { - javaClassPath += File.pathSeparatorChar + resolvedAdditionalDependency - .toURI().toString().substring("file:".length()); - } - } - classpath = javaClassPath; - logger.debug("Creating iterable for class path: {}", classpath); - } - Key k = new Key(location, classpath, packageName, kinds, recurse); - CloseableFilterableJavaFileObjectIterable resultIterable = this.iterables.get(k); - if (resultIterable == null) { - if (moduleRootPath != null) { - resultIterable = new IterableJrtModule(this.compilationInfoCache, - moduleRootPath, packageName, recurse); - } - else { - resultIterable = new IterableClasspath(this.compilationInfoCache, - classpath, packageName, recurse); - } - this.iterables.put(k, resultIterable); - } - resultIterable.reset(); - return resultIterable; - } - - private String getClassPath() { - if (this.classpath == null) { - ClassLoader loader = InMemoryJavaFileObject.class.getClassLoader(); - String cp = null; - if (loader instanceof URLClassLoader) { - cp = classPath((URLClassLoader) loader, cp); - } - if (cp == null) { - cp = System.getProperty("java.class.path"); - } - if (hasJrtFsPath()) { - cp = cp + File.pathSeparator + getJrtFsPath(); - } - this.classpath = pathWithPlatformClassPathRemoved(cp); - } - return this.classpath; - } - - private String classPath(URLClassLoader loader, String cp) { - URL[] urls = loader.getURLs(); - if (urls.length > 1) { // heuristic that catches Maven surefire tests - if (!urls[0].toString().startsWith("jar:file:")) { // heuristic for - // Spring Boot fat - // jar - StringBuilder builder = new StringBuilder(); - for (URL url : urls) { - if (builder.length() > 0) { - builder.append(File.pathSeparator); - } - String path = url.toString(); - if (path.startsWith("file:")) { - path = path.substring("file:".length()); - } - builder.append(path); - } - cp = builder.toString(); - } - } - return cp; - } - - // remove the platform classpath entries, they will be search separately (and earlier) - private String pathWithPlatformClassPathRemoved(String classpath) { - Set pcps = toList(getPlatformClassPath()); - Set cps = toList(classpath); - cps.removeAll(pcps); - StringBuilder builder = new StringBuilder(); - for (String cpe : cps) { - if (builder.length() > 0) { - builder.append(File.pathSeparator); - } - builder.append(cpe); - } - return builder.toString(); - } - - private Set toList(String path) { - Set result = new LinkedHashSet<>(); - StringTokenizer tokenizer = new StringTokenizer(path, File.pathSeparator); - while (tokenizer.hasMoreTokens()) { - result.add(tokenizer.nextToken()); - } - return result; - } - - @Override - public boolean hasLocation(Location location) { - logger.debug("hasLocation({})", location); - return (location == StandardLocation.SOURCE_PATH - || location == StandardLocation.CLASS_PATH - || location == StandardLocation.PLATFORM_CLASS_PATH); - } - - @Override - public String inferBinaryName(Location location, JavaFileObject file) { - if (location == StandardLocation.SOURCE_PATH) { - return null; - } - // Kind of ignoring location here... assuming we want basically the FQ type name - // Example value from getName(): javax/validation/bootstrap/GenericBootstrap.class - String classname = file.getName().replace('/', '.').replace('\\', '.'); - return classname.substring(0, classname.lastIndexOf(".class")); - } - - @Override - public boolean isSameFile(FileObject a, FileObject b) { - logger.debug("isSameFile({},{})", a, b); - return a.equals(b); - } - - @Override - public boolean handleOption(String current, Iterator remaining) { - logger.debug("handleOption({},{})", current, remaining); - return false; // This file manager does not manage any options - } - - @Override - public JavaFileObject getJavaFileForInput(Location location, String className, - Kind kind) throws IOException { - logger.debug("getJavaFileForInput({},{},{})", location, className, kind); - // getJavaFileForInput(SOURCE_PATH,module-info,SOURCE) - if (className.equals("module-info")) { - return null; - } - throw new IllegalStateException("Not expected to be used in this context"); - } - - @Override - public JavaFileObject getJavaFileForOutput(Location location, String className, - Kind kind, FileObject sibling) throws IOException { - logger.debug("getJavaFileForOutput({},{},{},{})", location, className, kind, - sibling); - // Example parameters: CLASS_OUTPUT, Foo, CLASS, - // StringBasedJavaSourceFileObject[string:///a/b/c/Foo.java] - return this.outputCollector.getJavaFileForOutput(location, className, kind, - sibling); - } - - @Override - public FileObject getFileForInput(Location location, String packageName, - String relativeName) throws IOException { - logger.debug("getFileForInput({},{},{})", location, packageName, relativeName); - throw new IllegalStateException("Not expected to be used in this context"); - } - - @Override - public FileObject getFileForOutput(Location location, String packageName, - String relativeName, FileObject sibling) throws IOException { - logger.debug("getFileForOutput({},{},{},{})", location, packageName, relativeName, - sibling); - // This can be called when the annotation config processor runs - // Example parameters: CLASS_OUTPUT, , - // META-INF/spring-configuration-metadata.json, null - return this.outputCollector.getFileForOutput(location, packageName, relativeName, - sibling); - } - - @Override - public void flush() throws IOException { - } - - @Override - public void close() throws IOException { - Collection toClose = this.iterables - .values(); - for (CloseableFilterableJavaFileObjectIterable icp : toClose) { - icp.close(); - } - } - - public List getCompiledClasses() { - return this.outputCollector.getCompiledClasses(); - } - - public List addAndResolveDependencies(String[] dependencies) { - List resolutionMessages = new ArrayList<>(); - for (String dependency : dependencies) { - if (dependency.startsWith("maven:")) { - // Resolving an explicit external archive - String coordinates = dependency.replaceFirst("maven:\\/*", ""); - DependencyResolver engine = DependencyResolver.instance(); - try { - File resolved = engine.resolve( - new Dependency(new DefaultArtifact(coordinates), "runtime")); - // Example: - // dependency = - // maven://org.springframework:spring-expression:4.3.9.RELEASE - // resolved.toURI() = - // file:/Users/aclement/.m2/repository/ - // org/springframework/spring-expression/4.3.9.RELEASE/spring-expression-4.3.9.RELEASE.jar - this.resolvedAdditionalDependencies.put(dependency, resolved); - } - catch (RuntimeException re) { - CompilationMessage compilationMessage = new CompilationMessage( - CompilationMessage.Kind.ERROR, re.getMessage(), null, 0, 0); - resolutionMessages.add(compilationMessage); - } - } - else if (dependency.startsWith("file:")) { - this.resolvedAdditionalDependencies.put(dependency, - new File(URI.create(dependency))); - } - else { - resolutionMessages.add(new CompilationMessage( - CompilationMessage.Kind.ERROR, - "Unrecognized dependency: " + dependency - + " (expected something of the form: maven://groupId:artifactId:version)", - null, 0, 0)); - } - } - return resolutionMessages; - } - - public Map getResolvedAdditionalDependencies() { - return this.resolvedAdditionalDependencies; - } - - public String inferModuleName(Location location) throws IOException { - if (location instanceof JDKModuleLocation) { - JDKModuleLocation m = (JDKModuleLocation) location; - return m.getModuleName(); - } - throw new IllegalStateException( - "Asked to inferModuleName from a " + location.getClass().getName()); - } - - private boolean hasJrtFsPath() { - return getJrtFsPath() != null; - } - - private String getJrtFsPath() { - if (!this.checkedForJrtFsPath) { - String javaHome = System.getProperty("java.home"); - String jrtFsFilePath = javaHome + File.separator + "lib" + File.separator - + "jrt-fs.jar"; - File jrtFsFile = new File(jrtFsFilePath); - if (jrtFsFile.exists()) { - this.jrtFsFilePath = jrtFsFilePath; - } - this.checkedForJrtFsPath = true; - } - return this.jrtFsFilePath; - } - - public Iterable> listLocationsForModules(Location location) - throws IOException { - if (getJrtFsPath() != null - && location == StandardLocation.valueOf("SYSTEM_MODULES")) { - Set> ss = new HashSet<>(); - HashSet moduleLocations = new HashSet<>(); - ModuleIdentifierVisitor visitor = new ModuleIdentifierVisitor(); - Iterable roots = getJrtFs().getRootDirectories(); - try { - for (Path path : roots) { - Files.walkFileTree(path, visitor); - } - moduleLocations.addAll(visitor.getModuleLocations()); - } - catch (IOException ioe) { - throw new RuntimeException(ioe); - } - ss.add(moduleLocations); - return ss; - } - else { - return Collections.emptySet(); - } - } - - // Holds information that may help speed up compilation - static class CompilationInfoCache { - - private Map archivePackageCache; - - private boolean packageCacheInitialized = false; - - private Map packageCache = new HashMap(); - - private ArchiveInfo moduleArchiveInfo; - - ArchiveInfo getArchiveInfoFor(File archive) { - if (!archive.isFile() || !(archive.getName().endsWith(".zip") - || archive.getName().endsWith(".jar"))) { - // it is not an archive - return null; - } - if (this.archivePackageCache == null) { - this.archivePackageCache = new HashMap<>(); - } - try { - ArchiveInfo result = this.archivePackageCache.get(archive); - if (result == null) { - result = buildArchiveInfo(archive); - this.archivePackageCache.put(archive, result); - } - return result; - } - catch (Exception e) { - throw new IllegalStateException( - "Unexpected problem caching entries from " + archive.getName(), - e); - } - } - - private synchronized ArchiveInfo buildPackageMap() { - if (!this.packageCacheInitialized) { - this.packageCacheInitialized = true; - Iterable roots = getJrtFs().getRootDirectories(); - PackageCacheBuilderVisitor visitor = new PackageCacheBuilderVisitor(); - try { - for (java.nio.file.Path path : roots) { - Files.walkFileTree(path, visitor); - } - } - catch (IOException e) { - throw new RuntimeException(e); - } - List ls = new ArrayList<>(); - ls.addAll(this.packageCache.keySet()); - Collections.sort(ls); - this.moduleArchiveInfo = new ArchiveInfo(ls, false); - } - return this.moduleArchiveInfo; - } - - /** - * Walk the specified archive and collect up the package names of any .class files - * encountered. If the archive contains nested jars packaged in a BOOT style way - * (under a BOOT-INF/lib folder) then walk those too and include relevant - * packages. - * @param file archive file to discover packages from - * @return an ArchiveInfo encapsulating package info from the archive - */ - private ArchiveInfo buildArchiveInfo(File file) { - if (file.toString().endsWith("jrt-fs.jar")) { - // Special treatment for >=JDK9 - treat this as intention to use modules - return buildPackageMap(); - } - List packageNames = new ArrayList<>(); - boolean isBootJar = false; - try (ZipFile openArchive = new ZipFile(file)) { - Enumeration entries = openArchive.entries(); - while (entries.hasMoreElements()) { - ZipEntry entry = entries.nextElement(); - String name = entry.getName(); - if (name.endsWith(".class")) { - if (name.startsWith(BOOT_PACKAGING_PREFIX_FOR_CLASSES)) { - isBootJar = true; - int idx = name.lastIndexOf('/') + 1; - if (idx != 0) { - if (idx == BOOT_PACKAGING_PREFIX_FOR_CLASSES.length()) { - // default package - packageNames.add("/"); - } - else { - // Normalize to forward slashes - name = name.substring( - BOOT_PACKAGING_PREFIX_FOR_CLASSES.length(), - idx); - name = name.replace('\\', '/'); - packageNames.add(name); - } - } - } - else { - int idx = name.lastIndexOf('/') + 1; - if (idx != 0) { - // Normalize to forward slashes - name = name.replace('\\', '/'); - name = name.substring(0, idx); - packageNames.add(name); - } - else if (idx == 0) { - // default package entries in here - packageNames.add("/"); - } - } - } - else if (name.startsWith(BOOT_PACKAGING_PREFIX_FOR_LIBRARIES) - && name.endsWith(".jar")) { - isBootJar = true; - try (ZipInputStream zis = new ZipInputStream( - openArchive.getInputStream(entry))) { - Enumeration nestedZipEnumerator = new ZipEnumerator( - zis); - while (nestedZipEnumerator.hasMoreElements()) { - ZipEntry innerEntry = nestedZipEnumerator.nextElement(); - String innerEntryName = innerEntry.getName(); - if (innerEntryName.endsWith(".class")) { - int idx = innerEntryName.lastIndexOf('/') + 1; - if (idx != 0) { - // Normalize to forward slashes - innerEntryName = innerEntryName.replace('\\', - '/'); - innerEntryName = innerEntryName.substring(0, idx); - packageNames.add(innerEntryName); - } - else if (idx == 0) { - // default package entries in here - packageNames.add("/"); - } - } - } - } - } - } - } - catch (IOException ioe) { - throw new IllegalStateException( - "Unexpected problem determining packages in " + file, ioe); - } - return new ArchiveInfo(packageNames, isBootJar); - } - - static class ArchiveInfo { - - // The packages identified in a particular archive - private List packageNames; - - private boolean isBootJar = false; - - ArchiveInfo(List packageNames, boolean isBootJar) { - this.packageNames = packageNames; - Collections.sort(this.packageNames); - this.isBootJar = isBootJar; - } - - public List getPackageNames() { - return this.packageNames; - } - - public boolean isBootJar() { - return this.isBootJar; - } - - public boolean containsPackage(String packageName, - boolean subpackageMatchesAllowed) { - if (subpackageMatchesAllowed) { - for (String candidatePackageName : this.packageNames) { - if (candidatePackageName.startsWith(packageName)) { - return true; - } - } - return false; - } - else { - // Must be an exact match, fast binary search: - int pos = Collections.binarySearch(this.packageNames, packageName); - return (pos >= 0); - } - } - - } - - private class PackageCacheBuilderVisitor extends SimpleFileVisitor { - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - if (file.getNameCount() > 3 && file.toString().endsWith(".class")) { - int fnc = file.getNameCount(); - if (fnc > 3) { // There is a package name - e.g. - // /modules/java.base/java/lang/Object.class - Path packagePath = file.subpath(2, fnc - 1); // e.g. java/lang - String packagePathString = packagePath.toString() + "/"; - CompilationInfoCache.this.packageCache.put(packagePathString, - file.subpath(0, fnc - 1)); // java/lang - // -> - // /modules/java.base/java/lang - } - } - return FileVisitResult.CONTINUE; - } - - } - - } - - static class Key { - - private Location location; - - private String classpath; - - private String packageName; - - private Set kinds; - - private boolean recurse; - - Key(Location location, String classpath, String packageName, Set kinds, - boolean recurse) { - this.location = location; - this.classpath = classpath; - this.packageName = packageName; - this.kinds = kinds; - this.recurse = recurse; - } - - @Override - public int hashCode() { - return (((this.location.hashCode() * 37) + this.classpath.hashCode() * 37 - + (this.packageName == null ? 0 : this.packageName.hashCode())) * 37 - + this.kinds.hashCode()) * 37 + (this.recurse ? 1 : 0); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Key)) { - return false; - } - Key that = (Key) obj; - return this.location.equals(that.location) - && this.classpath.equals(that.classpath) - && this.kinds.equals(that.kinds) && (this.recurse == that.recurse) - && (this.packageName == null ? (that.packageName == null) - : this.packageName.equals(that.packageName)); - } - - } - - static class JDKModuleLocation implements Location { - - private String moduleName; - - private Path moduleRootPath; - - JDKModuleLocation(String moduleName, Path moduleRootPath) { - this.moduleName = moduleName; - this.moduleRootPath = moduleRootPath; - } - - @Override - public String getName() { - return "MODULE"; - } - - @Override - public boolean isOutputLocation() { - return false; - } - - public String getModuleName() { - return this.moduleName; - } - - public Path getModuleRootPath() { - return this.moduleRootPath; - } - - public String toString() { - return "JDKModuleLocation(" + this.moduleName + ")"; - } - - public int hashCode() { - return this.moduleName.hashCode(); - } - - public boolean equals(Object other) { - if (!(other instanceof JDKModuleLocation)) { - return false; - } - return this.hashCode() == ((JDKModuleLocation) other).hashCode(); - } - - } - - static class ModuleIdentifierVisitor extends SimpleFileVisitor { - - private Map modules = new HashMap<>(); - - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - throws IOException { - if (file.getNameCount() > 2 && file.toString().endsWith(".class")) { - // /modules/jdk.rmic/sun/tools/tree/CaseStatement.class - String moduleName = file.getName(1).toString(); // jdk.rmic - Path moduleRootPath = file.subpath(0, 2); // /modules/jdk.rmic - if (!this.modules.containsKey(moduleName)) { - this.modules.put(moduleName, moduleRootPath); - } - } - return FileVisitResult.CONTINUE; - } - - public Set getModuleLocations() { - if (this.modules.size() == 0) { - return Collections.emptySet(); - } - else { - Set locations = new HashSet<>(); - for (Map.Entry moduleEntry : this.modules.entrySet()) { - locations.add(new JDKModuleLocation(moduleEntry.getKey(), - moduleEntry.getValue())); - } - return locations; - } - } - - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/NestedZipEntryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/NestedZipEntryJavaFileObject.java deleted file mode 100644 index b78889dec..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/NestedZipEntryJavaFileObject.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.Writer; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipInputStream; - -import javax.lang.model.element.Modifier; -import javax.lang.model.element.NestingKind; -import javax.tools.JavaFileObject; - -/** - * Represents an element inside in zip which is itself inside a zip. These objects are not - * initially created with the content of the file they represent, only enough information - * to find that content because many will typically be created but only few will be - * opened. - * - * @author Andy Clement - */ -public class NestedZipEntryJavaFileObject implements JavaFileObject { - - private File outerFile; - - private ZipFile outerZipFile; - - private ZipEntry innerZipFile; - - private ZipEntry innerZipFileEntry; - - private URI uri; - - public NestedZipEntryJavaFileObject(File outerFile, ZipFile outerZipFile, - ZipEntry innerZipFile, ZipEntry innerZipFileEntry) { - this.outerFile = outerFile; - this.outerZipFile = outerZipFile; - this.innerZipFile = innerZipFile; - this.innerZipFileEntry = innerZipFileEntry; - } - - @Override - public String getName() { - return this.innerZipFileEntry.getName(); // Example: a/b/C.class - } - - @Override - public URI toUri() { - if (this.uri == null) { - String uriString = null; - try { - uriString = "zip:" + this.outerFile.getAbsolutePath() + "!" - + this.innerZipFile.getName() + "!" - + this.innerZipFileEntry.getName(); - this.uri = new URI(uriString); - } - catch (URISyntaxException e) { - throw new IllegalStateException( - "Unexpected URISyntaxException for string '" + uriString + "'", - e); - } - } - return this.uri; - } - - @Override - public InputStream openInputStream() throws IOException { - // Find the inner zip file inside the outer zip file, then - // find the relevant entry, then return the stream. - InputStream innerZipFileInputStream = this.outerZipFile - .getInputStream(this.innerZipFile); - ZipInputStream innerZipInputStream = new ZipInputStream(innerZipFileInputStream); - ZipEntry nextEntry = innerZipInputStream.getNextEntry(); - while (nextEntry != null) { - if (nextEntry.getName().equals(this.innerZipFileEntry.getName())) { - return innerZipInputStream; - } - nextEntry = innerZipInputStream.getNextEntry(); - } - throw new IllegalStateException( - "Unable to locate nested zip entry " + this.innerZipFileEntry.getName() - + " in zip " + this.innerZipFile.getName() + " inside zip " - + this.outerZipFile.getName()); - } - - @Override - public Reader openReader(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "getCharContent() not supported on class file: " + getName()); - } - - @Override - public long getLastModified() { - return this.innerZipFileEntry.getTime(); - } - - @Override - public Kind getKind() { - // The filtering before this object was created ensure it is only used for classes - return Kind.CLASS; - } - - @Override - public boolean delete() { - return false; // Cannot delete entries inside nested zips - } - - @Override - public OutputStream openOutputStream() throws IOException { - throw new IllegalStateException("cannot write to nested zip entry: " + toUri()); - } - - @Override - public Writer openWriter() throws IOException { - throw new IllegalStateException("cannot write to nested zip entry: " + toUri()); - } - - @Override - public boolean isNameCompatible(String simpleName, Kind kind) { - if (kind != Kind.CLASS) { - return false; - } - String name = getName(); - int lastSlash = name.lastIndexOf('/'); - return name.substring(lastSlash + 1).equals(simpleName + ".class"); - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "getCharContent() not supported on class file: " + getName()); - } - - @Override - public NestingKind getNestingKind() { - return null; // nesting level not known - } - - @Override - public Modifier getAccessLevel() { - return null; // access level not known - } - - @Override - public int hashCode() { - int hc = this.outerFile.getName().hashCode(); - hc = hc * 37 + this.innerZipFile.getName().hashCode(); - hc = hc * 37 + this.innerZipFileEntry.getName().hashCode(); - return hc; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof NestedZipEntryJavaFileObject)) { - return false; - } - NestedZipEntryJavaFileObject that = (NestedZipEntryJavaFileObject) obj; - return (this.outerFile.getName().equals(that.outerFile.getName())) - && (this.innerZipFile.getName().equals(that.innerZipFile.getName())) - && (this.innerZipFileEntry.getName() - .equals(that.innerZipFileEntry.getName())); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java deleted file mode 100644 index 699dda570..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompiler.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.tools.Diagnostic; -import javax.tools.Diagnostic.Kind; -import javax.tools.DiagnosticCollector; -import javax.tools.JavaCompiler; -import javax.tools.JavaCompiler.CompilationTask; -import javax.tools.JavaFileObject; -import javax.tools.ToolProvider; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Compile Java source at runtime and load it. - * - * @author Andy Clement - */ -public class RuntimeJavaCompiler { - - private static Logger logger = LoggerFactory.getLogger(RuntimeJavaCompiler.class); - - private JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - - /** - * Compile the named class consisting of the supplied source code. If successful load - * the class and return it. Multiple classes may get loaded if the source code - * included anonymous/inner/local classes. - * @param className the name of the class (dotted form, e.g. com.foo.bar.Goo) - * @param classSourceCode the full source code for the class - * @param dependencies optional coordinates for dependencies, maven - * 'maven://groupId:artifactId:version', or 'file:' URIs for local files - * @return a CompilationResult that encapsulates what happened during compilation - * (classes/messages produced) - */ - public CompilationResult compile(String className, String classSourceCode, - String... dependencies) { - logger.info("Compiling source for class {} using compiler {}", className, - this.compiler.getClass().getName()); - - DiagnosticCollector diagnosticCollector = new DiagnosticCollector(); - MemoryBasedJavaFileManager fileManager = new MemoryBasedJavaFileManager(); - List resolutionMessages = fileManager - .addAndResolveDependencies(dependencies); - JavaFileObject sourceFile = InMemoryJavaFileObject - .getSourceJavaFileObject(className, classSourceCode); - - Iterable compilationUnits = Arrays.asList(sourceFile); - List options = new ArrayList<>(); - options.add("-source"); - options.add("1.8"); - CompilationTask task = this.compiler.getTask(null, fileManager, - diagnosticCollector, options, null, compilationUnits); - - boolean success = task.call(); - CompilationResult compilationResult = new CompilationResult(success); - compilationResult.recordCompilationMessages(resolutionMessages); - compilationResult.setResolvedAdditionalDependencies(new ArrayList<>( - fileManager.getResolvedAdditionalDependencies().values())); - - // If successful there may be no errors but there might be info/warnings - for (Diagnostic diagnostic : diagnosticCollector - .getDiagnostics()) { - CompilationMessage.Kind kind = (diagnostic.getKind() == Kind.ERROR - ? CompilationMessage.Kind.ERROR : CompilationMessage.Kind.OTHER); - // String sourceCode = - // ((StringBasedJavaSourceFileObject)diagnostic.getSource()).getSourceCode(); - String sourceCode = null; - try { - sourceCode = (String) diagnostic.getSource().getCharContent(true); - } - catch (IOException ioe) { - // Unexpected, but leave sourceCode null to indicate it was not - // retrievable - } - catch (NullPointerException npe) { - // TODO: should we skip warning diagnostics in the loop altogether? - } - int startPosition = (int) diagnostic.getPosition(); - if (startPosition == Diagnostic.NOPOS) { - startPosition = (int) diagnostic.getStartPosition(); - } - CompilationMessage compilationMessage = new CompilationMessage(kind, - diagnostic.getMessage(null), sourceCode, startPosition, - (int) diagnostic.getEndPosition()); - compilationResult.recordCompilationMessage(compilationMessage); - } - if (success) { - List ccds = fileManager.getCompiledClasses(); - List> classes = new ArrayList<>(); - try (SimpleClassLoader ccl = new SimpleClassLoader( - this.getClass().getClassLoader())) { - for (CompiledClassDefinition ccd : ccds) { - Class clazz = ccl.defineClass(ccd.getClassName(), ccd.getBytes()); - classes.add(clazz); - compilationResult.addClassBytes(ccd.getClassName(), ccd.getBytes()); - } - } - catch (IOException ioe) { - logger.debug("Unexpected exception defining classes", ioe); - } - compilationResult.setCompiledClasses(classes); - } - return compilationResult; - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java deleted file mode 100644 index 797e55538..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/SimpleClassLoader.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.List; - -/** - * Very simple classloader that can be used to load the compiled types. - * - * @author Andy Clement - */ -public class SimpleClassLoader extends URLClassLoader { - - private static final URL[] NO_URLS = new URL[0]; - - public SimpleClassLoader(ClassLoader classLoader) { - super(NO_URLS, classLoader); - } - - public SimpleClassLoader(List resolvedAdditionalDependencies, - ClassLoader classLoader) { - super(toUrls(resolvedAdditionalDependencies), classLoader); - } - - private static URL[] toUrls(List resolvedAdditionalDependencies) { - URL[] urls = new URL[resolvedAdditionalDependencies.size()]; - for (int i = 0, max = resolvedAdditionalDependencies.size(); i < max; i++) { - try { - urls[i] = resolvedAdditionalDependencies.get(i).toURI().toURL(); - } - catch (Exception e) { - throw new IllegalStateException(e); - } - } - return urls; - } - - public Class defineClass(String name, byte[] bytes) { - return super.defineClass(name, bytes, 0, bytes.length); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/ZipEntryJavaFileObject.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/ZipEntryJavaFileObject.java deleted file mode 100644 index 306300aec..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/java/ZipEntryJavaFileObject.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.Writer; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import javax.lang.model.element.Modifier; -import javax.lang.model.element.NestingKind; -import javax.tools.JavaFileObject; - -/** - * A {@link JavaFileObject} that works on a ZIP entry. - * - * @author Mark Fisher - */ -public class ZipEntryJavaFileObject implements JavaFileObject { - - private File containingFile; - - private ZipFile zf; - - private ZipEntry ze; - - private URI uri; - - public ZipEntryJavaFileObject(File containingFile, ZipFile zipFile, ZipEntry entry) { - this.containingFile = containingFile; - this.zf = zipFile; - this.ze = entry; - } - - @Override - public URI toUri() { - if (this.uri == null) { - String uriString = null; - try { - uriString = "zip:" + this.containingFile.getAbsolutePath() + "!" - + this.ze.getName(); - this.uri = new URI(uriString); - } - catch (URISyntaxException e) { - throw new IllegalStateException( - "Unexpected URISyntaxException for string '" + uriString + "'", - e); - } - } - return this.uri; - } - - @Override - public String getName() { - return this.ze.getName(); // a/b/C.class - } - - @Override - public InputStream openInputStream() throws IOException { - return this.zf.getInputStream(this.ze); - } - - @Override - public OutputStream openOutputStream() throws IOException { - throw new IllegalStateException("only expected to be used for input"); - } - - @Override - public Reader openReader(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "openReader() not supported on class file: " + getName()); - } - - @Override - public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { - // It is bytecode - throw new UnsupportedOperationException( - "getCharContent() not supported on class file: " + getName()); - } - - @Override - public Writer openWriter() throws IOException { - throw new IllegalStateException("only expected to be used for input"); - } - - @Override - public long getLastModified() { - return this.ze.getTime(); - } - - @Override - public boolean delete() { - return false; // Cannot delete entries inside zips - } - - @Override - public Kind getKind() { - return Kind.CLASS; - } - - @Override - public boolean isNameCompatible(String simpleName, Kind kind) { - if (kind != Kind.CLASS) { - return false; - } - String name = getName(); - int lastSlash = name.lastIndexOf('/'); - return name.substring(lastSlash + 1).equals(simpleName + ".class"); - } - - @Override - public NestingKind getNestingKind() { - return null; - } - - @Override - public Modifier getAccessLevel() { - return null; - } - - @Override - public int hashCode() { - int hc = this.containingFile.getName().hashCode(); - hc = hc * 37 + this.ze.getName().hashCode(); - return hc; - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof ZipEntryJavaFileObject)) { - return false; - } - ZipEntryJavaFileObject that = (ZipEntryJavaFileObject) obj; - return (this.containingFile.getName().equals(that.containingFile.getName())) - && (this.ze.getName().equals(that.ze.getName())); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractByteCodeLoadingProxy.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractByteCodeLoadingProxy.java deleted file mode 100644 index 0d1b2f472..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractByteCodeLoadingProxy.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicReference; - -import org.springframework.asm.ClassReader; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.cloud.function.compiler.CompilationResultFactory; -import org.springframework.cloud.function.compiler.java.SimpleClassLoader; -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.core.io.Resource; -import org.springframework.util.FileCopyUtils; -import org.springframework.util.ReflectionUtils; - -/** - * @author Mark Fisher - * @author Oleg Zhurakousky - */ -abstract class AbstractByteCodeLoadingProxy - implements InitializingBean, FunctionFactoryMetadata { - - private final Resource resource; - - private final SimpleClassLoader classLoader = new SimpleClassLoader( - AbstractByteCodeLoadingProxy.class.getClassLoader()); - - private T target; - - private Method method; - - AbstractByteCodeLoadingProxy(Resource resource) { - this.resource = resource; - } - - @Override - @SuppressWarnings("unchecked") - public void afterPropertiesSet() throws Exception { - byte[] bytes = FileCopyUtils.copyToByteArray(this.resource.getInputStream()); - String className = new ClassReader(bytes).getClassName().replace("/", "."); - Class factoryClass = this.classLoader.defineClass(className, bytes); - try { - this.target = ((CompilationResultFactory) factoryClass.newInstance()) - .getResult(); - this.method = findFactoryMethod(factoryClass); - } - catch (InstantiationException | IllegalAccessException e) { - throw new IllegalArgumentException("failed to load Function byte code", e); - } - } - - @Override - public final T getTarget() { - return this.target; - } - - @Override - public Method getFactoryMethod() { - return this.method; - } - - private Method findFactoryMethod(Class clazz) { - AtomicReference method = new AtomicReference<>(); - ReflectionUtils.doWithLocalMethods(clazz, m -> { - if (m.getName().equals("getResult") - && m.getReturnType().getName().startsWith("java.util.function")) { - method.set(m); - } - }); - return method.get(); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractLambdaCompilingProxy.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractLambdaCompilingProxy.java deleted file mode 100644 index 235501bc9..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/AbstractLambdaCompilingProxy.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.io.InputStreamReader; -import java.lang.reflect.Method; - -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.cloud.function.compiler.AbstractFunctionCompiler; -import org.springframework.cloud.function.compiler.CompiledFunctionFactory; -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.core.io.Resource; -import org.springframework.util.Assert; -import org.springframework.util.FileCopyUtils; - -/** - * @param target type - * @author Mark Fisher - */ -public class AbstractLambdaCompilingProxy - implements InitializingBean, BeanNameAware, FunctionFactoryMetadata { - - private final Resource resource; - - private final AbstractFunctionCompiler compiler; - - private String beanName; - - private CompiledFunctionFactory factory; - - private String[] typeParameterizations; - - public AbstractLambdaCompilingProxy(Resource resource, - AbstractFunctionCompiler compiler) { - Assert.notNull(resource, "Resource must not be null"); - Assert.notNull(compiler, "Compiler must not be null"); - this.resource = resource; - this.compiler = compiler; - } - - @Override - public void setBeanName(String beanName) { - this.beanName = beanName; - } - - public void setTypeParameterizations(String... typeParameterizations) { - this.typeParameterizations = typeParameterizations; - } - - @Override - public void afterPropertiesSet() throws Exception { - String lambda = FileCopyUtils - .copyToString(new InputStreamReader(this.resource.getInputStream())); - this.factory = this.compiler.compile(this.beanName, lambda, - this.typeParameterizations); - } - - @Override - public final T getTarget() { - return this.factory.getResult(); - } - - @Override - public Method getFactoryMethod() { - return this.factory.getFactoryMethod(); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingConsumer.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingConsumer.java deleted file mode 100644 index da45e2e47..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingConsumer.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.util.function.Consumer; - -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.core.io.Resource; - -/** - * @param type - * @author Mark Fisher - * @author Oleg Zhurakousky - */ -public class ByteCodeLoadingConsumer extends AbstractByteCodeLoadingProxy> - implements FunctionFactoryMetadata>, Consumer { - - public ByteCodeLoadingConsumer(Resource resource) { - super(resource); - } - - @Override - public void accept(T t) { - this.getTarget().accept(t); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunction.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunction.java deleted file mode 100644 index 19e921c8c..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.util.function.Function; - -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.core.io.Resource; - -/** - * @param Function input type - * @param Function result type - * @author Mark Fisher - * @author Oleg Zhurakousky - */ -public class ByteCodeLoadingFunction - extends AbstractByteCodeLoadingProxy> - implements FunctionFactoryMetadata>, Function { - - public ByteCodeLoadingFunction(Resource resource) { - super(resource); - } - - @Override - public R apply(T input) { - return this.getTarget().apply(input); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingSupplier.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingSupplier.java deleted file mode 100644 index 257fab96b..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingSupplier.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.util.function.Supplier; - -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.core.io.Resource; - -/** - * @param type - * @author Mark Fisher - * @author Oleg Zhurakousky - */ -public class ByteCodeLoadingSupplier extends AbstractByteCodeLoadingProxy> - implements FunctionFactoryMetadata>, Supplier { - - public ByteCodeLoadingSupplier(Resource resource) { - super(resource); - } - - @Override - public T get() { - return this.getTarget().get(); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingConsumer.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingConsumer.java deleted file mode 100644 index bd8c5e16b..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingConsumer.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.util.function.Consumer; - -import org.springframework.cloud.function.compiler.ConsumerCompiler; -import org.springframework.core.io.Resource; - -/** - * @param input argument type - * @author Mark Fisher - */ -public class LambdaCompilingConsumer extends AbstractLambdaCompilingProxy> - implements Consumer { - - public LambdaCompilingConsumer(Resource resource, ConsumerCompiler compiler) { - super(resource, compiler); - } - - @Override - public void accept(T input) { - this.getTarget().accept(input); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingFunction.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingFunction.java deleted file mode 100644 index 92a01d21a..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingFunction.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.util.function.Function; - -import org.springframework.cloud.function.compiler.FunctionCompiler; -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.core.io.Resource; - -/** - * @param input argument type - * @param output argument type - * @author Mark Fisher - */ -public class LambdaCompilingFunction - extends AbstractLambdaCompilingProxy> - implements FunctionFactoryMetadata>, Function { - - public LambdaCompilingFunction(Resource resource, FunctionCompiler compiler) { - super(resource, compiler); - } - - @Override - public R apply(T input) { - return this.getTarget().apply(input); - } - -} diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingSupplier.java b/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingSupplier.java deleted file mode 100644 index cb2db39d9..000000000 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/proxy/LambdaCompilingSupplier.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.util.function.Supplier; - -import org.springframework.cloud.function.compiler.SupplierCompiler; -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.core.io.Resource; - -/** - * @param target type - * @author Mark Fisher - */ -public class LambdaCompilingSupplier extends AbstractLambdaCompilingProxy> - implements FunctionFactoryMetadata>, Supplier { - - public LambdaCompilingSupplier(Resource resource, SupplierCompiler compiler) { - super(resource, compiler); - } - - @Override - public T get() { - return this.getTarget().get(); - } - -} diff --git a/spring-cloud-function-compiler/src/main/resources/META-INF/spring.factories b/spring-cloud-function-compiler/src/main/resources/META-INF/spring.factories deleted file mode 100644 index 0f7f11139..000000000 --- a/spring-cloud-function-compiler/src/main/resources/META-INF/spring.factories +++ /dev/null @@ -1,2 +0,0 @@ -org.springframework.context.ApplicationListener=\ -org.springframework.cloud.function.compiler.config.FunctionProxyApplicationListener diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/CompilerDependencyResolutionTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/CompilerDependencyResolutionTests.java deleted file mode 100644 index 5480e8a9c..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/CompilerDependencyResolutionTests.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.jar.JarOutputStream; -import java.util.zip.ZipEntry; - -import ch.qos.logback.classic.Level; -import ch.qos.logback.classic.Logger; -import ch.qos.logback.classic.LoggerContext; -import org.junit.jupiter.api.Test; -import org.slf4j.LoggerFactory; - -import org.springframework.cloud.function.compiler.java.CompilationResult; -import org.springframework.cloud.function.compiler.java.RuntimeJavaCompiler; -import org.springframework.cloud.function.core.FunctionFactoryUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests that verify dependency resolution. Dependencies can be resolved against simple - * classpath entries or against classes under BOOT-INF/classes or in a nested jar under - * under BOOT-INF/lib. Finding classes in those locations enables compilation against a - * packaged boot jar. - * - * @author Andy Clement - */ -public class CompilerDependencyResolutionTests { - - @Test - public void compilingTestClass() throws Exception { - ClassDescriptor t1 = compile("Test1", - "package com.test;\npublic class Test1 { public static String doit() { return \"T1\";}}\n"); - String result = (String) t1.clazz.getDeclaredMethod("doit").invoke(null); - assertThat(result).isEqualTo("T1"); - } - - @Test - public void packagingClassesIntoJar() { - ClassDescriptor t1 = getTestClass("1"); - ClassDescriptor t2 = getTestClass("2"); - File jar = JarBuilder.create().addEntries(t1, t2).getJar(); - assertJarContents(jar, t1, t2); - } - - /** - * Doesn't actually verify the caching helps but can be useful to run to see current - * numbers. - */ - @Test - public void speedtest() { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - Logger rootLogger = loggerContext - .getLogger("org.springframework.cloud.function.compiler"); - rootLogger.setLevel(Level.ERROR); - - // 10 uses of a single function compiler: - long stime = System.currentTimeMillis(); - FunctionCompiler fc = new FunctionCompiler( - String.class.getName()); - for (int i = 0; i < 5; i++) { - stime = System.currentTimeMillis(); - CompiledFunctionFactory> result = fc.compile("foos", - "flux -> flux.map(v -> v.toUpperCase())", "Flux", - "Flux"); - assertThat(FunctionFactoryUtils.isFluxFunction(result.getFactoryMethod())) - .isTrue(); - System.out.println("Reusing FunctionCompiler: #" + (i + 1) + " = " - + (System.currentTimeMillis() - stime) + "ms"); - } - - // 3 separate FunctionCompilers: - stime = System.currentTimeMillis(); - CompiledFunctionFactory> compiled = new FunctionCompiler( - String.class.getName()).compile("foos", - "flux -> flux.map(v -> v.toUpperCase())", "Flux", - "Flux"); - assertThat(FunctionFactoryUtils.isFluxFunction(compiled.getFactoryMethod())) - .isTrue(); - long etime = System.currentTimeMillis(); - long time1 = (etime - stime); - System.out.println("New FunctionCompiler: " + time1 + "ms"); - - stime = System.currentTimeMillis(); - compiled = new FunctionCompiler(String.class.getName()).compile( - "foos", "flux -> flux.map(v -> v.toUpperCase())", "Flux", - "Flux"); - assertThat(FunctionFactoryUtils.isFluxFunction(compiled.getFactoryMethod())) - .isTrue(); - etime = System.currentTimeMillis(); - long time2 = (etime - stime); - System.out.println("New FunctionCompiler: " + time2 + "ms"); - - stime = System.currentTimeMillis(); - compiled = new FunctionCompiler(String.class.getName()).compile( - "foos", "flux -> flux.map(v -> v.toUpperCase())", "Flux", - "Flux"); - assertThat(FunctionFactoryUtils.isFluxFunction(compiled.getFactoryMethod())) - .isTrue(); - etime = System.currentTimeMillis(); - long time3 = (etime - stime); - System.out.println("New FunctionCompiler: " + time3 + "ms"); - } - - @Test - public void usingJarNoPackageDecl() throws Exception { - ClassDescriptor tx = compile("TestX", - "public class TestX { public static String doit() { return \"TX\";}}\n"); - File jar = JarBuilder.create().addEntry(tx).getJar(); - assertJarContents(jar, tx); - CompilationResult result = new RuntimeJavaCompiler().compile("A", - "public class A {\n" + " public static Object run() {\n" - + " return new TestX();\n" + " }\n" + "}", - jar.toURI().toString()); - assertThat(result.getCompilationMessages().isEmpty()) - .as("Should be no problems: " + result.getCompilationMessages()).isTrue(); - try (URLClassLoader cl = new TestClassLoader(tx, descriptorFromResult(result))) { - Class class1 = cl.loadClass("A"); - Object invoke = class1.getDeclaredMethod("run").invoke(null); - assertThat(invoke.getClass().getName()).isEqualTo(tx.name); - } - } - - // A class with no package declaration is placed under BOOT-INF/classes/ in a jar that - // is then used for resolution - @Test - public void usingJarNoPackageDeclBootInfClasses() throws Exception { - ClassDescriptor t1 = compile("TestX", - "public class TestX { public static String doit() { return \"TX\";}}\n"); - File jar = JarBuilder.create().addEntryWithPrefix("BOOT-INF/classes/", t1) - .getJar(); - assertJarContents(jar, "BOOT-INF/classes/", t1); - CompilationResult result = new RuntimeJavaCompiler().compile("A", - "public class A {\n" + " public static Object run() {\n" - + " return new TestX();\n" + " }\n" + "}", - jar.toURI().toString()); - assertThat(result.getCompilationMessages().isEmpty()) - .as("Should be no problems: " + result.getCompilationMessages()).isTrue(); - try (URLClassLoader cl = new TestClassLoader(t1, descriptorFromResult(result))) { - Class class1 = cl.loadClass("A"); - Object invoke = class1.getDeclaredMethod("run").invoke(null); - assertThat(invoke.getClass().getName()).isEqualTo(t1.name); - } - } - - // A class with no package declaration is placed in a jar which is then placed under - // under BOOT-INF/lib/ in a jar that is then used for resolution - @Test - public void usingJarNoPackageDeclNestedBootInfLib() throws Exception { - ClassDescriptor t1 = compile("TestX", - "public class TestX { public static String doit() { return \"TX\";}}\n"); - File jar = JarBuilder.create().addEntry(t1).getJar(); - assertJarContents(jar, t1); - // Now stick that jar in another jar! - File jar2 = JarBuilder.create().addEntry("BOOT-INF/lib/inner.jar", jar).getJar(); - CompilationResult result = new RuntimeJavaCompiler().compile("A", - "public class A {\n" + " public static Object run() {\n" - + " return new TestX();\n" + " }\n" + "}", - jar2.toURI().toString()); - assertThat(result.getCompilationMessages().isEmpty()) - .as("Should be no problems: " + result.getCompilationMessages()).isTrue(); - try (URLClassLoader cl = new TestClassLoader(t1, descriptorFromResult(result))) { - Class class1 = cl.loadClass("A"); - Object invoke = class1.getDeclaredMethod("run").invoke(null); - assertThat(invoke.getClass().getName()).isEqualTo(t1.name); - } - } - - // Build a jar containing a type with a package declaration and building against it - @Test - public void usingJarWithPackageDecl() throws Exception { - ClassDescriptor t1 = getTestClass("1"); - File jar = JarBuilder.create().addEntry(t1).getJar(); - assertJarContents(jar, t1); - CompilationResult result = new RuntimeJavaCompiler().compile("A", - "import " + t1.name.replace('$', '.') + ";\n" + "public class A {\n" - + " public static Object run() {\n" + " return new Test1();\n" - + " }\n" + "}", - jar.toURI().toString()); - assertThat(result.getCompilationMessages().isEmpty()) - .as("Should be no problems: " + result.getCompilationMessages()).isTrue(); - try (URLClassLoader cl = new TestClassLoader(t1, descriptorFromResult(result))) { - Class class1 = cl.loadClass("A"); - Object invoke = class1.getDeclaredMethod("run").invoke(null); - assertThat(invoke.getClass().getName()).isEqualTo(t1.name); - } - } - - @Test - public void usingJarWithPackageDeclBootInfClasses() throws Exception { - // Here the dependencies are under BOOT-INF/classes in the jar - ClassDescriptor t1 = getTestClass("1"); - File jar = JarBuilder.create().addEntryWithPrefix("BOOT-INF/classes/", t1) - .getJar(); - assertJarContents(jar, "BOOT-INF/classes/", t1); - CompilationResult result = new RuntimeJavaCompiler().compile("A", - "import " + t1.name.replace('$', '.') + ";\n" + "public class A {\n" - + " public static Object run() {\n" + " return new Test1();\n" - + " }\n" + "}", - jar.toURI().toString()); - assertThat(result.getCompilationMessages().isEmpty()) - .as("Should be no problems: " + result.getCompilationMessages()).isTrue(); - try (URLClassLoader cl = new TestClassLoader(t1, descriptorFromResult(result))) { - Class class1 = cl.loadClass("A"); - Object invoke = class1.getDeclaredMethod("run").invoke(null); - assertThat(invoke.getClass().getName()).isEqualTo(t1.name); - } - } - - @Test - public void usingJarWithPackageDeclNestedBootInfLib() throws Exception { - // Here the dependencies are under BOOT-INF/lib/nested.jar - ClassDescriptor t1 = getTestClass("1"); - File jar = JarBuilder.create().addEntry(t1).getJar(); - assertJarContents(jar, t1); - // Now stick that jar in another jar! - File jar2 = JarBuilder.create().addEntry("BOOT-INF/lib/inner.jar", jar).getJar(); - CompilationResult result = new RuntimeJavaCompiler().compile("A", - "import " + t1.name.replace('$', '.') + ";\n" + "public class A {\n" - + " public static Object run() {\n" + " return new Test1();\n" - + " }\n" + "}", - jar2.toURI().toString()); - assertThat(result.getCompilationMessages().isEmpty()) - .as("Should be no problems: " + result.getCompilationMessages()).isTrue(); - try (URLClassLoader cl = new TestClassLoader(t1, descriptorFromResult(result))) { - Class class1 = cl.loadClass("A"); - Object invoke = class1.getDeclaredMethod("run").invoke(null); - assertThat(invoke.getClass().getName()).isEqualTo(t1.name); - } - } - - // --- - - private ClassDescriptor descriptorFromResult(CompilationResult result) { - Class clazz = result.getCompiledClasses().get(0); - return new ClassDescriptor(clazz.getName(), result.getClassBytes(clazz.getName()), - clazz); - } - - private ClassDescriptor compile(String className, String classSourceCode) { - CompilationResult compile = new RuntimeJavaCompiler().compile(className, - classSourceCode); - assertThat(compile.getCompilationMessages().isEmpty()) - .as("Should be empty: \n" + compile.getCompilationMessages()).isTrue(); - Class clazz = compile.getCompiledClasses().get(0); - return new ClassDescriptor(clazz.getName(), - compile.getClassBytes(clazz.getName()), - compile.getCompiledClasses().get(0)); - } - - private ClassDescriptor getTestClass(String suffix) { - try { - return compile("Test" + suffix, - "package com.test;\npublic class Test" + suffix - + " { public static String doit() { return \"T" + suffix - + "\";}}\n"); - } - catch (Exception e) { - throw new IllegalStateException(e); - } - } - - private void assertJarContents(File jar, ClassDescriptor... classdescriptors) { - assertJarContents(jar, "", classdescriptors); - } - - private void assertJarContents(File jar, String prefix, - ClassDescriptor... classDescriptors) { - List clazzes = new ArrayList<>(); - for (ClassDescriptor classDescriptor : classDescriptors) { - clazzes.add(prefix + classDescriptor.name.replace('.', '/') + ".class"); - } - walkJar(jar, (entry) -> clazzes.remove(entry.getName())); - assertThat(clazzes.isEmpty()).as("Should be empty: " + clazzes).isTrue(); - } - - private void walkJar(File jar, Consumer fn) { - try { - JarInputStream jarInputStream = new JarInputStream(new FileInputStream(jar)); - while (true) { - JarEntry nextJarEntry = jarInputStream.getNextJarEntry(); - if (nextJarEntry == null) { - break; - } - fn.accept(nextJarEntry); - } - jarInputStream.close(); - } - catch (IOException ioe) { - ioe.printStackTrace(); - } - } - - @SuppressWarnings("unused") - private void printJar(File jar) { - System.out.println("Contents of jar: " + jar); - walkJar(jar, (entry) -> { - System.out.println("- " + entry.getName()); - }); - } - - // Simple holder for the result of compilation - static class ClassDescriptor { - - final String name; - - final byte[] bytes; - - final Class clazz; - - ClassDescriptor(String name, byte[] bytes, Class clazz) { - this.name = name; - this.bytes = bytes; - this.clazz = clazz; - } - - } - - static final class JarBuilder { - - File jarFile; - - JarOutputStream jos; - - private JarBuilder() { - try { - File newJar = File.createTempFile("test", ".jar"); - this.jarFile = newJar.getAbsoluteFile(); - newJar.delete(); - this.jos = new JarOutputStream(new FileOutputStream(this.jarFile)); - this.jarFile.deleteOnExit(); - } - catch (IOException e) { - throw new IllegalStateException("Unexpected problem creating file", e); - } - } - - public static JarBuilder create() { - return new JarBuilder(); - } - - public JarBuilder addEntry(String entryName, File entryContentFile) { - try { - ZipEntry ze = new ZipEntry(entryName); - this.jos.putNextEntry(ze); - this.jos.write(loadBytes(entryContentFile)); - this.jos.closeEntry(); - return this; - } - catch (IOException e) { - throw new IllegalStateException(e); - } - } - - private byte[] loadBytes(File f) { - try (InputStream is = new FileInputStream(f)) { - byte[] bs = null; - byte[] buf = new byte[10000]; - int readCount; - while ((readCount = is.read(buf)) != -1) { - if (bs == null) { - bs = new byte[readCount]; - System.arraycopy(buf, 0, bs, 0, readCount); - } - else { - byte[] newbs = new byte[bs.length + readCount]; - System.arraycopy(bs, 0, newbs, 0, bs.length); - System.arraycopy(buf, 0, newbs, bs.length, readCount); - bs = newbs; - } - } - return bs; - } - catch (IOException ioe) { - throw new IllegalStateException(ioe); - } - } - - public JarBuilder addEntries(ClassDescriptor... classes) { - for (ClassDescriptor clazz : classes) { - addEntry(clazz); - } - return this; - } - - public JarBuilder addEntry(ClassDescriptor clazz) { - return addEntryWithPrefix("", clazz); - } - - public JarBuilder addEntryWithPrefix(String prefix, ClassDescriptor holder) { - try { - String n = holder.name.replace('.', '/') + ".class"; - ZipEntry ze = new ZipEntry(prefix + n); - this.jos.putNextEntry(ze); - this.jos.write(holder.bytes); - this.jos.closeEntry(); - return this; - } - catch (IOException e) { - throw new IllegalStateException(e); - } - } - - private File getJar() { - try { - this.jos.close(); - } - catch (IOException e) { - throw new IllegalStateException("Unable to close jar", e); - } - return this.jarFile; - } - - } - - // Simple classloader that can load from descriptors - class TestClassLoader extends URLClassLoader { - - ClassDescriptor[] descriptors; - - TestClassLoader(ClassDescriptor... descriptors) { - super(new URL[0], TestClassLoader.class.getClassLoader()); - this.descriptors = descriptors; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - for (ClassDescriptor descriptor : this.descriptors) { - if (descriptor.name.equals(name)) { - return defineClass(descriptor.name, descriptor.bytes, 0, - descriptor.bytes.length); - } - } - return super.findClass(name); - } - - } - -} diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/ConsumerCompilerTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/ConsumerCompilerTests.java deleted file mode 100644 index 962823b13..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/ConsumerCompilerTests.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.util.function.Consumer; - -import org.junit.jupiter.api.Test; - -import org.springframework.cloud.function.core.FunctionFactoryUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Dave Syer - * - */ -public class ConsumerCompilerTests { - - @Test - public void consumesFluxString() { - CompiledFunctionFactory> compiled = new ConsumerCompiler( - String.class.getName()).compile("foos", - "flux -> flux.subscribe(System.out::println)", "Flux"); - assertThat(FunctionFactoryUtils.isFluxConsumer(compiled.getFactoryMethod())) - .isTrue(); - } - - @Test - public void consumesString() { - CompiledFunctionFactory> compiled = new ConsumerCompiler( - String.class.getName()).compile("foos", "System.out::println", "String"); - assertThat(FunctionFactoryUtils.isFluxConsumer(compiled.getFactoryMethod())) - .isFalse(); - } - -} diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/FunctionCompilerTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/FunctionCompilerTests.java deleted file mode 100644 index 0a00deffc..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/FunctionCompilerTests.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.util.function.Function; - -import org.junit.jupiter.api.Test; - -import org.springframework.cloud.function.core.FunctionFactoryUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Dave Syer - * - */ -public class FunctionCompilerTests { - - @Test - public void transformsFluxString() { - CompiledFunctionFactory> compiled = new FunctionCompiler( - String.class.getName()).compile("foos", - "flux -> flux.map(v -> v.toUpperCase())", "Flux", - "Flux"); - assertThat(FunctionFactoryUtils.isFluxFunction(compiled.getFactoryMethod())) - .isTrue(); - } - - @Test - public void transformsString() { - CompiledFunctionFactory> compiled = new FunctionCompiler( - String.class.getName()).compile("foos", "v -> v.toUpperCase()", "String", - "String"); - assertThat(FunctionFactoryUtils.isFluxFunction(compiled.getFactoryMethod())) - .isFalse(); - assertThat(compiled.getResult().apply("hello")).isEqualTo("HELLO"); - } - -} diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/SupplierCompilerTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/SupplierCompilerTests.java deleted file mode 100644 index fa2a0a225..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/SupplierCompilerTests.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler; - -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.cloud.function.core.FunctionFactoryUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Dave Syer - * - */ -public class SupplierCompilerTests { - - @Test - public void supppliesFluxString() { - CompiledFunctionFactory> compiled = new SupplierCompiler( - String.class.getName()).compile("foos", - "() -> Flux.just(\"foo\", \"bar\")", "Flux"); - assertThat(FunctionFactoryUtils.isFluxSupplier(compiled.getFactoryMethod())) - .isTrue(); - } - - @Test - public void supppliesString() { - CompiledFunctionFactory> compiled = new SupplierCompiler( - String.class.getName()).compile("foos", "() -> \"foo\"", "String"); - assertThat(FunctionFactoryUtils.isFluxSupplier(compiled.getFactoryMethod())) - .isFalse(); - assertThat(compiled.getResult().get()).isEqualTo("foo"); - } - - @Test - public void supppliesFluxStreamString() { - CompiledFunctionFactory>> compiled = new SupplierCompiler>( - String.class.getName()).compile("foos", - "() -> Flux.interval(Duration.ofMillis(1000)).map(Object::toString)", - "Flux"); - assertThat(FunctionFactoryUtils.isFluxSupplier(compiled.getFactoryMethod())) - .isTrue(); - assertThat(compiled.getResult().get().blockFirst()).isEqualTo("0"); - } - -} diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompilerTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompilerTests.java deleted file mode 100644 index 10ec123eb..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/java/RuntimeJavaCompilerTests.java +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.java; - -import java.io.File; -import java.util.List; -import java.util.Locale; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Andy Clement - */ -@SuppressWarnings("unchecked") -public class RuntimeJavaCompilerTests { - - @Test - public void basicCompilation() { - RuntimeJavaCompiler rjc = new RuntimeJavaCompiler(); - CompilationResult cr = rjc.compile("A", "public class A {}"); - List compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.isEmpty()).isTrue(); - } - - @Test - public void missingType() throws Exception { - Locale.setDefault(Locale.ENGLISH); - RuntimeJavaCompiler rjc = new RuntimeJavaCompiler(); - CompilationResult cr = rjc.compile("A", - "public class A implements java.util.function.Supplier { " - + " public String get() {\n" - + " ExpressionParser parser = new SpelExpressionParser();\n" - + " Expression exp = parser.parseExpression(\"'Hello World'\");\n" - + " String message = (String) exp.getValue();" - + " return message;\n" + " }\n" + "}"); - List compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.size()).isEqualTo(3); - assertThat(compilationMessages.get(0).getMessage().contains("cannot find symbol")) - .isTrue(); - assertThat(compilationMessages.get(0).getMessage() - .contains("class ExpressionParser")).isTrue(); - assertThat(compilationMessages.get(1).getMessage().contains("cannot find symbol")) - .isTrue(); - assertThat(compilationMessages.get(1).getMessage() - .contains("class SpelExpressionParser")).isTrue(); - assertThat(compilationMessages.get(2).getMessage().contains("cannot find symbol")) - .isTrue(); - assertThat(compilationMessages.get(2).getMessage().contains("class Expression")) - .isTrue(); - } - - @Test - public void okWithImportedDependencies() throws Exception { - RuntimeJavaCompiler rjc = new RuntimeJavaCompiler(); - CompilationResult cr = rjc.compile("A", - "import org.springframework.expression.*;\n" - + "import org.springframework.expression.spel.standard.*;\n" - + "public class A implements java.util.function.Supplier {\n" - + " public String get() {\n" - + " ExpressionParser parser = new SpelExpressionParser();\n" - + " Expression exp = parser.parseExpression(\"'Hello World'\");\n" - + " String message = (String) exp.getValue();\n" - + " return message;\n" + " }\n" + "}", - "maven://org.springframework:spring-expression:4.3.9.RELEASE"); - List compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.isEmpty()).isTrue(); - try (SimpleClassLoader cl = new SimpleClassLoader( - this.getClass().getClassLoader())) { - Class clazz = cl.defineClass("A", cr.getClassBytes("A")); - Supplier supplier = (Supplier) clazz.newInstance(); - assertThat(supplier.get()).isEqualTo("Hello World"); - } - } - - @Test - public void okWithImportedDependencies2() throws Exception { - RuntimeJavaCompiler rjc = new RuntimeJavaCompiler(); - String source = "import org.joda.time.*;\n" - + "public class A implements java.util.function.Supplier {\n" - + " public String get() {\n" + " DateTime dt = new DateTime();\n" - + " int month = dt.getMonthOfYear();\n" - + " return String.valueOf(month>0);\n" + " }\n" + "}"; - CompilationResult cr = rjc.compile("A", source, - "maven://joda-time:joda-time:2.9.9"); - List compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.isEmpty()).isTrue(); - List resolvedAdditionalDependencies = cr - .getResolvedAdditionalDependencies(); - try (SimpleClassLoader cl = new SimpleClassLoader(resolvedAdditionalDependencies, - this.getClass().getClassLoader())) { - Class clazz = cl.defineClass("A", cr.getClassBytes("A")); - Supplier supplier = (Supplier) clazz.newInstance(); - assertThat(supplier.get()).isEqualTo("true"); - } - - cr = rjc.compile("A", source, - "maven://org.springframework:spring-expression:4.3.9.RELEASE", - "maven://joda-time:joda-time:2.9.9"); - compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.isEmpty()).isTrue(); - resolvedAdditionalDependencies = cr.getResolvedAdditionalDependencies(); - try (SimpleClassLoader cl = new SimpleClassLoader(resolvedAdditionalDependencies, - this.getClass().getClassLoader())) { - Class clazz = cl.defineClass("A", cr.getClassBytes("A")); - Supplier supplier = (Supplier) clazz.newInstance(); - assertThat(supplier.get()).isEqualTo("true"); - } - } - - @Test - public void dependencyResolution() throws Exception { - // Failure: - RuntimeJavaCompiler rjc = new RuntimeJavaCompiler(); - CompilationResult cr = rjc.compile("A", "public class A {}", - "maven://org.springframework:spring-expression2:4.3.9.RELEASE"); // extra - // '2' - // in - // there - List compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.size()).isEqualTo(1); - // ERROR:org.eclipse.aether.resolution.ArtifactResolutionException: Could not find - // artifact org.springframework:spring-expression2:jar:4.3.9.RELEASE in - // spring-snapshots (https://repo.spring.io/libs-snapshot) - assertThat(compilationMessages.get(0).getMessage().contains( - "Could not find artifact org.springframework:spring-expression2:jar:4.3.9.RELEASE")) - .isTrue(); - - // Failure: - rjc = new RuntimeJavaCompiler(); - cr = rjc.compile("A", "public class A {}", - "trouble://org.springframework:spring-expression:4.3.9.RELEASE"); // rogue - // prefix - // (should - // be - // "maven:") - compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.size()).isEqualTo(1); - assertThat(compilationMessages.get(0).getMessage() - .contains("Unrecognized dependency: ")) - .as(compilationMessages.get(0).toString()).isTrue(); - - // Success - rjc = new RuntimeJavaCompiler(); - cr = rjc.compile("A", "public class A {}", "maven://joda-time:joda-time:2.9.9"); - compilationMessages = cr.getCompilationMessages(); - assertThat(compilationMessages.size()).isEqualTo(0); - List resolvedAdditionalDependencies = cr - .getResolvedAdditionalDependencies(); - assertThat(resolvedAdditionalDependencies.size()).isEqualTo(1); - assertThat(resolvedAdditionalDependencies.get(0).toString() - .endsWith("joda-time-2.9.9.jar")) - .as("Expected this to end with 'joda-time-2.9.9.jar': " - + resolvedAdditionalDependencies.get(0).toString()) - .isTrue(); - } - -} diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunctionTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunctionTests.java deleted file mode 100644 index 47d86b89f..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/compiler/proxy/ByteCodeLoadingFunctionTests.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.compiler.proxy; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.cloud.function.compiler.CompiledFunctionFactory; -import org.springframework.cloud.function.compiler.ConsumerCompiler; -import org.springframework.cloud.function.compiler.FunctionCompiler; -import org.springframework.cloud.function.compiler.SupplierCompiler; -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.cloud.function.core.FunctionFactoryUtils; -import org.springframework.core.io.ByteArrayResource; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Dave Syer - * @author Oleg Zhurakousky - */ -public class ByteCodeLoadingFunctionTests { - - @Test - public void compileConsumer() throws Exception { - CompiledFunctionFactory> compiled = new ConsumerCompiler( - String.class.getName()).compile("foos", "System.out::println", "String"); - ByteArrayResource resource = new ByteArrayResource( - compiled.getGeneratedClassBytes(), "foos"); - ByteCodeLoadingConsumer consumer = new ByteCodeLoadingConsumer<>( - resource); - consumer.afterPropertiesSet(); - assertThat(consumer instanceof FunctionFactoryMetadata); - assertThat(FunctionFactoryUtils.isFluxConsumer(consumer.getFactoryMethod())) - .isFalse(); - consumer.accept("foo"); - } - - @Test - public void compileSupplier() throws Exception { - CompiledFunctionFactory> compiled = new SupplierCompiler( - String.class.getName()).compile("foos", "() -> \"foo\"", "String"); - ByteArrayResource resource = new ByteArrayResource( - compiled.getGeneratedClassBytes(), "foos"); - ByteCodeLoadingSupplier supplier = new ByteCodeLoadingSupplier<>( - resource); - supplier.afterPropertiesSet(); - assertThat(supplier instanceof FunctionFactoryMetadata); - assertThat(FunctionFactoryUtils.isFluxSupplier(supplier.getFactoryMethod())) - .isFalse(); - assertThat(supplier.get()).isEqualTo("foo"); - } - - @Test - public void compileFunction() throws Exception { - CompiledFunctionFactory> compiled = new FunctionCompiler( - String.class.getName()).compile("foos", "v -> v.toUpperCase()", "String", - "String"); - ByteArrayResource resource = new ByteArrayResource( - compiled.getGeneratedClassBytes(), "foos"); - ByteCodeLoadingFunction function = new ByteCodeLoadingFunction<>( - resource); - function.afterPropertiesSet(); - assertThat(function instanceof FunctionFactoryMetadata); - assertThat(FunctionFactoryUtils.isFluxFunction(function.getFactoryMethod())) - .isFalse(); - assertThat(function.apply("foo")).isEqualTo("FOO"); - } - - @Test - public void compileFluxFunction() throws Exception { - CompiledFunctionFactory, Flux>> compiled = null; - compiled = new FunctionCompiler, Flux>( - String.class.getName()).compile("foos", - "flux -> flux.map(v -> v.toUpperCase())", "Flux", - "Flux"); - ByteArrayResource resource = new ByteArrayResource( - compiled.getGeneratedClassBytes(), "foos"); - ByteCodeLoadingFunction, Flux> function = new ByteCodeLoadingFunction<>( - resource); - function.afterPropertiesSet(); - assertThat(function instanceof FunctionFactoryMetadata); - assertThat(FunctionFactoryUtils.isFluxFunction(function.getFactoryMethod())) - .isTrue(); - assertThat(function.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO"); - } - -} diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/core/FunctionFactoryUtils.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/core/FunctionFactoryUtils.java deleted file mode 100644 index 41465e4a1..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/core/FunctionFactoryUtils.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.core; - -import java.lang.invoke.SerializedLambda; -import java.lang.reflect.Method; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; - -import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; - -/** - *

- * Miscellaneous utility operations to interrogate functional components (beans) - * configured in BeanFactory. - *

- *

- * It is important to understand that it is not a general purpose utility to interrogate - * "any" functional component. Certain operations may/will not work as expected due to - * java type erasure. While BeanFactory is not the requirement, this utility is targeting - * only the components defined in such way where they could be configured beans within - * BeanFactory. - *

- * It is primarily used internally by the framework. - * - * @author Mark Fisher - * @author Oleg Zhurakousky - */ -public abstract class FunctionFactoryUtils { - - private static final String FLUX_CLASS_NAME = Flux.class.getName(); - - private static final String PUBLISHER_CLASS_NAME = Publisher.class.getName(); - - private FunctionFactoryUtils() { - } - - public static boolean isFluxConsumer(Consumer consumer) { - return consumer instanceof FunctionFactoryMetadata - ? isFluxConsumer( - ((FunctionFactoryMetadata) consumer).getFactoryMethod()) - : isFlux(1, getParameterizedTypeNames(consumer, Consumer.class)); - } - - public static boolean isFluxConsumer(Method method) { - return isFlux(1, getParameterizedTypeNamesForMethod(method, Consumer.class)); - } - - public static boolean isFluxSupplier(Supplier supplier) { - return supplier instanceof FunctionFactoryMetadata - ? isFluxSupplier( - ((FunctionFactoryMetadata) supplier).getFactoryMethod()) - : isFlux(1, getParameterizedTypeNames(supplier, Supplier.class)); - } - - public static boolean isFluxSupplier(Method method) { - return isFlux(1, getParameterizedTypeNamesForMethod(method, Supplier.class)); - } - - public static boolean isFluxFunction(Function function) { - return function instanceof FunctionFactoryMetadata - ? isFluxFunction( - ((FunctionFactoryMetadata) function).getFactoryMethod()) - : isFlux(1, getParameterizedTypeNames(function, Function.class)); - } - - public static boolean isFluxFunction(Method method) { - return isFlux(2, getParameterizedTypeNamesForMethod(method, Function.class)); - } - - private static String[] getParameterizedTypeNamesForMethod(Method method, - Class interfaceClass) { - String[] types = retrieveTypes(method.getGenericReturnType(), interfaceClass); - return types == null ? new String[0] : types; - } - - private static String[] getParameterizedTypeNames(Object source, - Class interfaceClass) { - return Stream.of(source.getClass().getGenericInterfaces()) - .map(gi -> retrieveTypes(gi, interfaceClass)).filter(s -> s != null) - .findFirst().orElse(getSerializedLambdaParameterizedTypeNames(source)); - } - - private static String[] retrieveTypes(Type genericInterface, - Class interfaceClass) { - if ((genericInterface instanceof ParameterizedType) && interfaceClass - .getTypeName().equals(((ParameterizedType) genericInterface).getRawType() - .getTypeName())) { - ParameterizedType type = (ParameterizedType) genericInterface; - Type[] args = type.getActualTypeArguments(); - if (args != null) { - return Stream.of(args).map(arg -> arg.getTypeName()) - .toArray(String[]::new); - } - } - return null; - } - - private static String[] getSerializedLambdaParameterizedTypeNames(Object source) { - Method method = ReflectionUtils.findMethod(source.getClass(), "writeReplace"); - if (method == null) { - return null; - } - ReflectionUtils.makeAccessible(method); - SerializedLambda serializedLambda = (SerializedLambda) ReflectionUtils - .invokeMethod(method, source); - String signature = serializedLambda.getImplMethodSignature().replaceAll("[()]", - ""); - - List typeNames = Stream.of(signature.split(";")) - .map(t -> t.substring(1).replace('/', '.')).collect(Collectors.toList()); - - return typeNames.toArray(new String[typeNames.size()]); - } - - private static boolean isFlux(int length, String... types) { - return !ObjectUtils.isEmpty(types) && types.length == length - && Stream.of(types).allMatch(type -> type.startsWith(FLUX_CLASS_NAME) - || type.startsWith(PUBLISHER_CLASS_NAME)); - } - -} diff --git a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/core/FunctionFactoryUtilsTests.java b/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/core/FunctionFactoryUtilsTests.java deleted file mode 100644 index 2331285f8..000000000 --- a/spring-cloud-function-compiler/src/test/java/org/springframework/cloud/function/core/FunctionFactoryUtilsTests.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.core; - -import java.lang.reflect.Method; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.junit.jupiter.api.Test; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; - -import org.springframework.util.ReflectionUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Dave Syer - * - */ -public class FunctionFactoryUtilsTests { - - @Test - public void isFluxConsumer() { - Method method = ReflectionUtils.findMethod(FunctionFactoryUtilsTests.class, - "fluxConsumer"); - assertThat(FunctionFactoryUtils.isFluxConsumer(method)).isTrue(); - assertThat(FunctionFactoryUtils.isFluxSupplier(method)).isFalse(); - assertThat(FunctionFactoryUtils.isFluxFunction(method)).isFalse(); - } - - @Test - public void isFluxSupplier() { - Method method = ReflectionUtils.findMethod(FunctionFactoryUtilsTests.class, - "fluxSupplier"); - assertThat(FunctionFactoryUtils.isFluxSupplier(method)).isTrue(); - assertThat(FunctionFactoryUtils.isFluxConsumer(method)).isFalse(); - assertThat(FunctionFactoryUtils.isFluxFunction(method)).isFalse(); - } - - @Test - public void isFluxFunction() { - Method method = ReflectionUtils.findMethod(FunctionFactoryUtilsTests.class, - "fluxFunction"); - assertThat(FunctionFactoryUtils.isFluxFunction(method)).isTrue(); - assertThat(FunctionFactoryUtils.isFluxSupplier(method)).isFalse(); - assertThat(FunctionFactoryUtils.isFluxConsumer(method)).isFalse(); - } - - @Test - public void isReactiveFunction() { - Method method = ReflectionUtils.findMethod(FunctionFactoryUtilsTests.class, - "reactiveFunction"); - assertThat(FunctionFactoryUtils.isFluxFunction(method)).isTrue(); - assertThat(FunctionFactoryUtils.isFluxSupplier(method)).isFalse(); - assertThat(FunctionFactoryUtils.isFluxConsumer(method)).isFalse(); - } - - public Function, Flux> fluxFunction() { - return foos -> foos.map(foo -> new Foo()); - } - - public Function, Publisher> reactiveFunction() { - return foos -> Flux.from(foos).map(foo -> new Foo()); - } - - public Supplier> fluxSupplier() { - return () -> Flux.just(new Foo()); - } - - public Consumer> fluxConsumer() { - return flux -> flux.subscribe(System.out::println); - } - - class Foo { - - } - -} diff --git a/spring-cloud-function-compiler/src/test/resources/logback.xml b/spring-cloud-function-compiler/src/test/resources/logback.xml deleted file mode 100644 index 7d0a9651e..000000000 --- a/spring-cloud-function-compiler/src/test/resources/logback.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/spring-cloud-function-context/pom.xml b/spring-cloud-function-context/pom.xml index 2e8642d69..b3a5fd172 100644 --- a/spring-cloud-function-context/pom.xml +++ b/spring-cloud-function-context/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT @@ -127,7 +127,7 @@ kotlin-maven-plugin org.jetbrains.kotlin - 1.6.0 + 2.2.0 -Xjsr305=strict @@ -166,7 +166,7 @@ org.jetbrains.kotlin kotlin-maven-allopen - 1.6.0 + 2.2.0 diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageBuilder.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageBuilder.java index 3581c0ce3..f7f55ec45 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageBuilder.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageBuilder.java @@ -17,7 +17,7 @@ package org.springframework.cloud.function.cloudevent; import java.net.URI; -import java.time.OffsetTime; +import java.time.OffsetDateTime; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -111,13 +111,13 @@ public CloudEventMessageBuilder setSubject(String subject) { return this; } - public CloudEventMessageBuilder setTime(OffsetTime time) { + public CloudEventMessageBuilder setTime(OffsetDateTime time) { this.headers.put(CloudEventMessageUtils.TIME, time); return this; } public CloudEventMessageBuilder setTime(String time) { - this.headers.put(CloudEventMessageUtils.TIME, OffsetTime.parse(time)); + this.headers.put(CloudEventMessageUtils.TIME, OffsetDateTime.parse(time)); return this; } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtils.java index 24826f5a9..552ce4f12 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtils.java @@ -16,14 +16,15 @@ package org.springframework.cloud.function.cloudevent; -import java.lang.reflect.Field; import java.net.URI; -import java.time.OffsetTime; +import java.time.OffsetDateTime; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import org.springframework.cloud.function.context.message.MessageUtils; +import org.springframework.cloud.function.context.message.MessageUtils.MessageStructureWithCaseInsensitiveHeaderKeys; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -31,9 +32,9 @@ import org.springframework.messaging.converter.DefaultContentTypeResolver; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -60,12 +61,6 @@ public MimeType resolve(@Nullable MessageHeaders headers) { }; - private static Field MESSAGE_HEADERS = ReflectionUtils.findField(MessageHeaders.class, "headers"); - - static { - MESSAGE_HEADERS.setAccessible(true); - } - private CloudEventMessageUtils() { } @@ -207,9 +202,9 @@ public static String getSubject(Message message) { return (String) message.getHeaders().get(prefix + _SUBJECT); } - public static OffsetTime getTime(Message message) { + public static OffsetDateTime getTime(Message message) { String prefix = determinePrefixToUse(message.getHeaders()); - return (OffsetTime) message.getHeaders().get(prefix + _TIME); + return (OffsetDateTime) message.getHeaders().get(prefix + _TIME); } @SuppressWarnings("unchecked") @@ -232,17 +227,18 @@ public static Map getAttributes(Message message) { */ @SuppressWarnings("unchecked") static Message toCanonical(Message inputMessage, MessageConverter messageConverter) { - Map headers = (Map) ReflectionUtils.getField(MESSAGE_HEADERS, inputMessage.getHeaders()); + Map headers = new HashMap<>(inputMessage.getHeaders()); canonicalizeHeaders(headers, false); - if (isCloudEvent(inputMessage) && headers.containsKey("content-type")) { + boolean isCloudEvent = isCloudEvent(inputMessage); + if (isCloudEvent && headers.containsKey("content-type")) { inputMessage = MessageBuilder.fromMessage(inputMessage).setHeader(MessageHeaders.CONTENT_TYPE, headers.get("content-type")).build(); } String inputContentType = (String) inputMessage.getHeaders().get(DATACONTENTTYPE); + MimeType contentType = contentTypeResolver.resolve(inputMessage.getHeaders()); // first check the obvious and see if content-type is `cloudevents` - if (!isCloudEvent(inputMessage) && headers.containsKey(MessageHeaders.CONTENT_TYPE)) { + if (!isCloudEvent && contentType != null) { // structured-mode - MimeType contentType = contentTypeResolver.resolve(inputMessage.getHeaders()); if (contentType.getType().equals(APPLICATION_CLOUDEVENTS.getType()) && contentType .getSubtype().startsWith(APPLICATION_CLOUDEVENTS.getSubtype())) { @@ -257,7 +253,7 @@ static Message toCanonical(Message inputMessage, MessageConverter messageC .setHeader(DATACONTENTTYPE, dataContentType).build(); Map structuredCloudEvent = (Map) messageConverter .fromMessage(cloudEventMessage, Map.class); - + Assert.notEmpty(structuredCloudEvent, "Failed to convert CloudEvent from structured mode"); canonicalizeHeaders(structuredCloudEvent, true); return buildBinaryMessageFromStructuredMap(structuredCloudEvent, inputMessage.getHeaders()); @@ -268,7 +264,7 @@ else if (StringUtils.hasText(inputContentType)) { return MessageBuilder.fromMessage(inputMessage).setHeader(MessageHeaders.CONTENT_TYPE, inputContentType) .build(); } - return inputMessage; + return MessageBuilder.withPayload(inputMessage.getPayload()).copyHeaders(headers).build(); } @@ -331,21 +327,22 @@ else if (Protocols.HTTP.equals(targetProtocol)) { * @return true if this Message represents Cloud Event in binary-mode */ public static boolean isCloudEvent(Message message) { - return (message.getHeaders().containsKey(SPECVERSION) - && message.getHeaders().containsKey(TYPE) - && message.getHeaders().containsKey(SOURCE)) + MessageStructureWithCaseInsensitiveHeaderKeys _message = MessageUtils.toCaseInsensitiveHeadersStructure(message); + return (_message.getHeaders().containsKey(SPECVERSION) + && _message.getHeaders().containsKey(TYPE) + && _message.getHeaders().containsKey(SOURCE)) || - (message.getHeaders().containsKey(_SPECVERSION) - && message.getHeaders().containsKey(_TYPE) - && message.getHeaders().containsKey(_SOURCE)) + (_message.getHeaders().containsKey(_SPECVERSION) + && _message.getHeaders().containsKey(_TYPE) + && _message.getHeaders().containsKey(_SOURCE)) || - (message.getHeaders().containsKey(AMQP_ATTR_PREFIX + _SPECVERSION) - && message.getHeaders().containsKey(AMQP_ATTR_PREFIX + _TYPE) - && message.getHeaders().containsKey(AMQP_ATTR_PREFIX + _SOURCE)) + (_message.getHeaders().containsKey(AMQP_ATTR_PREFIX + _SPECVERSION) + && _message.getHeaders().containsKey(AMQP_ATTR_PREFIX + _TYPE) + && _message.getHeaders().containsKey(AMQP_ATTR_PREFIX + _SOURCE)) || - (message.getHeaders().containsKey(KAFKA_ATTR_PREFIX + _SPECVERSION) - && message.getHeaders().containsKey(KAFKA_ATTR_PREFIX + _TYPE) - && message.getHeaders().containsKey(KAFKA_ATTR_PREFIX + _SOURCE)); + (_message.getHeaders().containsKey(KAFKA_ATTR_PREFIX + _SPECVERSION) + && _message.getHeaders().containsKey(KAFKA_ATTR_PREFIX + _TYPE) + && _message.getHeaders().containsKey(KAFKA_ATTR_PREFIX + _SOURCE)); } private static boolean isAttribute(String key) { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionExtensionConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionExtensionConfiguration.java index 39a798e4e..89a424c63 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionExtensionConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionExtensionConfiguration.java @@ -30,7 +30,7 @@ * @since 3.1 */ @Configuration(proxyBeanMethods = false) -class CloudEventsFunctionExtensionConfiguration { +public class CloudEventsFunctionExtensionConfiguration { // The following two beans are intended to be mutually exclusive. Only one should be activated based // on the presence of Cloud Event SDK API diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionInvocationHelper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionInvocationHelper.java index 25331f28a..9d1b6fa2b 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionInvocationHelper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/cloudevent/CloudEventsFunctionInvocationHelper.java @@ -96,7 +96,15 @@ public Message postProcessResult(Object result, Message input) { if (this.messageConverter != null && CLOUD_EVENT_CLASS != null && CLOUD_EVENT_CLASS.isAssignableFrom(result.getClass())) { convertedResult = this.messageConverter.toMessage(result, input.getHeaders()); } - String targetPrefix = CloudEventMessageUtils.determinePrefixToUse(input.getHeaders(), true); + + String targetPrefix = CloudEventMessageUtils.DEFAULT_ATTR_PREFIX; + if (input != null) { + targetPrefix = CloudEventMessageUtils.determinePrefixToUse(input.getHeaders(), true); + } + else if (result instanceof Message) { + targetPrefix = CloudEventMessageUtils.determinePrefixToUse(((Message) result).getHeaders(), true); + } + Assert.hasText(targetPrefix, "Unable to determine prefix for Cloud Event atttributes, " + "which they must have according to protocol specification. Consider adding 'target-protocol' " + "header with values of one of the supported protocols - [kafka, amqp, http]"); diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/DefaultMessageRoutingHandler.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/DefaultMessageRoutingHandler.java new file mode 100644 index 000000000..87c1b7ec7 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/DefaultMessageRoutingHandler.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context; + +import java.util.function.Consumer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.function.context.config.RoutingFunction; +import org.springframework.messaging.Message; + +/** + * Strategy for implementing a handler for un-routable messages. + * Works in parallel with {@link RoutingFunction}. When registered as a bean, RoutingFunction will not throw + * an exception if it can not route message and instead such message will be routed to this function. + * Its default implementation simply logs the un-routable event. + * Users are encouraged to provide their own implementation of this class. + * + * @author Oleg Zhurakousky + * @since 3.2.9 + * + */ +public class DefaultMessageRoutingHandler implements Consumer> { + + Log logger = LogFactory.getLog(DefaultMessageRoutingHandler.class); + + @Override + public void accept(Message message) { + if (logger.isDebugEnabled()) { + logger.debug("Route-to function can not be located in FunctionCatalog. Dropping unroutable message: " + message + ""); + } + else { + logger.warn("Route-to function can not be located in FunctionCatalog. Droping message"); + } + } +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionProperties.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionProperties.java index 12f49a1e8..e9727d508 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionProperties.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionProperties.java @@ -16,7 +16,10 @@ package org.springframework.cloud.function.context; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -54,6 +57,11 @@ public class FunctionProperties implements EnvironmentAware, ApplicationContextA */ public final static String EXPECT_CONTENT_TYPE_HEADER = "expected-content-type"; + /** + * SpEL expression to be used with RoutingFunction. + */ + public final static String ROUTING_EXPRESSION = PREFIX + ".routing-expression"; + /** * The name of function definition property. */ @@ -65,6 +73,17 @@ public class FunctionProperties implements EnvironmentAware, ApplicationContextA */ private String definition; + /** + * SpEL expression which should result in function definition (e.g., function name or composition instruction). + * NOTE: SpEL evaluation context's root object is the input argument (e.g., Message). + */ + private String routingExpression; + + /** + * List of functions that are not eligible to be registered in Function Catalog. + */ + private final List ineligibleDefinitions; + private Map configuration; private String expectedContentType; @@ -73,16 +92,27 @@ public class FunctionProperties implements EnvironmentAware, ApplicationContextA private ApplicationContext applicationContext; + public FunctionProperties() { + ineligibleDefinitions = new ArrayList<>(); + String[] definitions = new String[] { + "org.springframework.boot", + "org.springframework.cloud.function.cloudevent.CloudEventsFunctionExtensionConfiguration", + "org.springframework.cloud.function.context.config.FunctionsEndpointAutoConfiguration", + "classLoaderMetrics", + "jvmMemoryMetrics", + "jvmInfoMetrics", + "jvmCompilationMetrics", + "uptimeMetrics", + "kotlinToFunctionTransformer", + "CloudEventsMessageConverterConfiguration" + }; + ineligibleDefinitions.addAll(Arrays.asList(definitions)); + } + public Map getConfiguration() { return configuration; } - /** - * SpEL expression which should result in function definition (e.g., function name or composition instruction). - * NOTE: SpEL evaluation context's root object is the input argument (e.g., Message). - */ - private String routingExpression; - @SuppressWarnings({ "unchecked", "rawtypes" }) public void setConfiguration(Map configuration) { for (Entry entry : configuration.entrySet()) { @@ -164,12 +194,22 @@ public void setEnvironment(Environment environment) { this.environment = environment; } + public List getIneligibleDefinitions() { + return new ArrayList<>(this.ineligibleDefinitions); + } + + public void setIneligibleDefinitions(List definitions) { + this.ineligibleDefinitions.addAll(definitions); + } + public static class FunctionConfigurationProperties { private Map inputHeaderMappingExpression; private Map outputHeaderMappingExpression; + private boolean copyInputHeaders; + public Map getInputHeaderMappingExpression() { return inputHeaderMappingExpression; } @@ -187,5 +227,13 @@ public void setOutputHeaderMappingExpression( this.outputHeaderMappingExpression = outputHeaderMappingExpression; } + public boolean isCopyInputHeaders() { + return copyInputHeaders; + } + + public void setCopyInputHeaders(boolean copyInputHeaders) { + this.copyInputHeaders = copyInputHeaders; + } + } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java index 536ce9ac2..b28a9c0b2 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java @@ -29,20 +29,11 @@ import net.jodah.typetools.TypeResolver; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import org.springframework.beans.factory.BeanNameAware; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; -import org.springframework.cloud.function.context.config.RoutingFunction; -import org.springframework.cloud.function.core.FluxConsumer; -import org.springframework.cloud.function.core.FluxFunction; -import org.springframework.cloud.function.core.FluxSupplier; -import org.springframework.cloud.function.core.FluxToMonoFunction; -import org.springframework.cloud.function.core.FluxedConsumer; -import org.springframework.cloud.function.core.FluxedFunction; -import org.springframework.cloud.function.core.MonoSupplier; -import org.springframework.cloud.function.core.MonoToFluxFunction; +import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration; +import org.springframework.core.KotlinDetector; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -123,7 +114,10 @@ public FunctionRegistration type(Type type) { } public FunctionRegistration type(FunctionType type) { - + this.type = type; + if (KotlinDetector.isKotlinPresent() && this.target instanceof KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper) { + return this; + } Type t = FunctionTypeUtils.discoverFunctionTypeFromClass(this.target.getClass()); if (t == null) { // only valid for Kafka Stream KStream[] return type. return null; @@ -137,7 +131,6 @@ public FunctionRegistration type(FunctionType type) { throw new IllegalStateException("Discovered function type does not match provided function type. Discovered: " + discoveredFunctionType + "; Provided: " + type); } - this.type = type; return this; } @@ -166,6 +159,13 @@ public FunctionRegistration names(String... names) { return this.names(Arrays.asList(names)); } + @Override + public void setBeanName(String name) { + if (CollectionUtils.isEmpty(this.names)) { + this.name(name); + } + } + /** * Transforms (wraps) function identified by the 'target' to its {@code Flux} * equivalent unless it already is. For example, {@code Function} @@ -175,66 +175,11 @@ public FunctionRegistration names(String... names) { * */ - @SuppressWarnings({ "unchecked", "rawtypes" }) - public FunctionRegistration wrap() { - this.isFunctionSignatureSupported(); - FunctionRegistration result; - if (this.type == null) { - result = (FunctionRegistration) this; - } - else if (this.target instanceof RoutingFunction) { - S target = (S) this.target; - result = new FunctionRegistration(target); - result.type(this.type.getType()); - result = result.target(target).names(this.names) - .type(result.type.wrap(Flux.class)).properties(this.properties); - } - else { - S target = (S) this.target; - result = new FunctionRegistration(target); - result.type(this.type.getType()); - - if (!this.type.isWrapper()) { - target = target instanceof Supplier - ? (S) new FluxSupplier((Supplier) target) - : target instanceof Function - ? (S) new FluxFunction((Function) target) - : (S) new FluxConsumer((Consumer) target); - } - else if (Mono.class.isAssignableFrom(this.type.getOutputWrapper())) { - target = target instanceof Supplier - ? (S) new MonoSupplier((Supplier) target) - : (S) new FluxToMonoFunction((Function) target); - } - else if (Mono.class.isAssignableFrom(this.type.getInputWrapper())) { - target = (S) new MonoToFluxFunction((Function) target); - } - else if (target instanceof Consumer) { - target = (S) new FluxedConsumer((Consumer) target); - } - else if (target instanceof Function) { - target = (S) new FluxedFunction((Function) target); - } - result = result.target(target).names(this.names) - .type(result.type.wrap(Flux.class)).properties(this.properties); - } - - return result; - } - - @Override - public void setBeanName(String name) { - if (CollectionUtils.isEmpty(this.names)) { - this.name(name); - } - } - - private void isFunctionSignatureSupported() { - if (type != null) { - Assert.isTrue(!(Mono.class.isAssignableFrom(this.type.getOutputWrapper()) - && Mono.class.isAssignableFrom(this.type.getInputWrapper())), - "Function is not supported."); - } - } +// @Override +// public void setBeanName(String name) { +// if (CollectionUtils.isEmpty(this.names)) { +// this.name(name); +// } +// } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java index eebf2f0d2..ed59e7e6f 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java @@ -96,6 +96,7 @@ protected void postProcessApplicationContext(ConfigurableApplicationContext cont Assert.isInstanceOf(GenericApplicationContext.class, context, "ApplicationContext must be an instanceof GenericApplicationContext"); for (Object source : getAllSources()) { + System.out.println("======> SOURCE: " + source); Class type = null; Object handler = null; if (source instanceof String) { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/AbstractComposableFunctionRegistry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/AbstractComposableFunctionRegistry.java deleted file mode 100644 index f0b934c9f..000000000 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/AbstractComposableFunctionRegistry.java +++ /dev/null @@ -1,439 +0,0 @@ -/* - * Copyright 2019-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.cloud.function.context.catalog; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import reactor.core.publisher.Flux; - -import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.FunctionRegistry; -import org.springframework.cloud.function.context.FunctionType; -import org.springframework.cloud.function.context.config.RoutingFunction; -import org.springframework.cloud.function.core.FluxToMonoFunction; -import org.springframework.cloud.function.core.IsolatedConsumer; -import org.springframework.cloud.function.core.IsolatedFunction; -import org.springframework.cloud.function.core.IsolatedSupplier; -import org.springframework.cloud.function.core.MonoToFluxFunction; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.context.EnvironmentAware; -import org.springframework.core.env.Environment; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -/** - * Base implementation of {@link FunctionRegistry} which supports function composition - * during lookups. For example if this registry contains function 'a' and 'b' you can - * compose them into a single function by simply piping two names together during the - * lookup {@code this.lookup(Function.class, "a|b")}. - * - * Comma ',' is also supported as composition delimiter (e.g., {@code "a,b"}). - * - * @author Oleg Zhurakousky - * @author Dave Syer - * @since 2.1 - * - */ -public abstract class AbstractComposableFunctionRegistry implements FunctionRegistry, - ApplicationEventPublisherAware, EnvironmentAware { - - private final Map functions = new ConcurrentHashMap<>(); - - private final Map names = new ConcurrentHashMap<>(); - - private final Map types = new ConcurrentHashMap<>(); - - private Environment environment = new StandardEnvironment(); - - protected ApplicationEventPublisher applicationEventPublisher; - - @SuppressWarnings("unchecked") - @Override - public T lookup(Class type, String name) { - String functionDefinitionName = !StringUtils.hasText(name) - && this.environment.containsProperty("spring.cloud.function.definition") - ? this.environment.getProperty("spring.cloud.function.definition") - : name; - return (T) this.doLookup(type, functionDefinitionName); - } - - @SuppressWarnings("serial") - @Override - public Set getNames(Class type) { - if (type == null) { - return new HashSet(getSupplierNames()) { - { - addAll(getFunctionNames()); - } - }; - } - if (Supplier.class.isAssignableFrom(type)) { - return this.getSupplierNames(); - } - if (Function.class.isAssignableFrom(type)) { - return this.getFunctionNames(); - } - return Collections.emptySet(); - } - - /** - * Returns the names of available Suppliers. - * @return immutable {@link Set} of available {@link Supplier} names. - */ - public Set getSupplierNames() { - return this.functions.entrySet().stream() - .filter(entry -> entry.getValue() instanceof Supplier) - .map(entry -> entry.getKey()) - .collect(Collectors.toSet()); - } - - /** - * Returns the names of available Functions. - * @return immutable {@link Set} of available {@link Function} names. - */ - public Set getFunctionNames() { - return this.functions.entrySet().stream() - .filter(entry -> !(entry.getValue() instanceof Supplier)) - .map(entry -> entry.getKey()) - .collect(Collectors.toSet()); - } - - public boolean hasSuppliers() { - return !CollectionUtils.isEmpty(getSupplierNames()); - } - - public boolean hasFunctions() { - return !CollectionUtils.isEmpty(getFunctionNames()); - } - - /** - * The size of this catalog, which is the count of all Suppliers, - * Function and Consumers currently registered. - * - * @return the count of all Suppliers, Function and Consumers currently registered. - */ - @Override - public int size() { - return this.functions.size(); - } - - public FunctionType getFunctionType(String name) { - return this.types.get(name); - } - - /** - * A reverse lookup where one can determine the actual name of the function reference. - * @param function should be an instance of {@link Supplier}, {@link Function} or - * {@link Consumer}; - * @return the name of the function or null. - */ - public String lookupFunctionName(Object function) { - return this.names.containsKey(function) ? this.names.get(function) : null; - } - - @Override - public void setApplicationEventPublisher( - ApplicationEventPublisher applicationEventPublisher) { - this.applicationEventPublisher = applicationEventPublisher; - } - - @Override - public void setEnvironment(Environment environment) { - this.environment = environment; - } - - - public FunctionRegistration getRegistration(Object function) { - String functionName = function == null ? null - : this.lookupFunctionName(function); - if (StringUtils.hasText(functionName)) { - FunctionRegistration registration = new FunctionRegistration( - function, functionName); - FunctionType functionType = this.findType(registration, functionName); - return registration.type(functionType.getType()); - } - return null; - } - - @Override - public void register(FunctionRegistration functionRegistration) { - Assert.notEmpty(functionRegistration.getNames(), - "'registration' must contain at least one name before it is registered in catalog."); - register(functionRegistration, functionRegistration.getNames().iterator().next()); - } - - - - /** - * Registers function wrapped by the provided FunctionRegistration with - * this FunctionRegistry. - * - * @param registration instance of {@link FunctionRegistration} - * @param key the name of the function - */ - protected void register(FunctionRegistration registration, String key) { - Object target = registration.getTarget(); - if (registration.getType() != null) { - this.addType(key, registration.getType()); - } - else { - FunctionType functionType = findType(registration, key); - if (functionType == null) { - return; // TODO fixme - } - this.addType(key, functionType); - registration.type(functionType.getType()); - } - Class type; - registration = isolated(registration).wrap(); - target = registration.getTarget(); - if (target instanceof Supplier) { - type = Supplier.class; - for (String name : registration.getNames()) { - this.addSupplier(name, (Supplier) registration.getTarget()); - } - } - else if (target instanceof Function) { - type = Function.class; - for (String name : registration.getNames()) { - this.addFunction(name, (Function) registration.getTarget()); - } - } - else { - return; - } - this.addName(registration.getTarget(), key); - if (this.applicationEventPublisher != null) { - this.applicationEventPublisher.publishEvent(new FunctionRegistrationEvent( - registration.getTarget(), type, registration.getNames())); - } - } - - protected FunctionType findType(FunctionRegistration functionRegistration, String name) { - return functionRegistration.getType() != null - ? functionRegistration.getType() - : this.getFunctionType(name); - } - - - protected void addSupplier(String name, Supplier supplier) { - this.functions.put(name, supplier); - } - - protected void addFunction(String name, Function function) { - this.functions.put(name, function); - } - - protected void addType(String name, FunctionType functionType) { - this.types.computeIfAbsent(name, str -> functionType); - } - - protected void addName(Object function, String name) { - this.names.put(function, name); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private FunctionRegistration isolated(FunctionRegistration input) { - FunctionRegistration registration = (FunctionRegistration) input; - Object target = registration.getTarget(); - boolean isolated = getClass().getClassLoader() != target.getClass() - .getClassLoader(); - if (isolated) { - if (target instanceof Supplier && isolated) { - target = new IsolatedSupplier((Supplier) target); - } - else if (target instanceof Function) { - target = new IsolatedFunction((Function) target); - } - else if (target instanceof Consumer) { - target = new IsolatedConsumer((Consumer) target); - } - } - - registration.target(target); - return registration; - } - - private Object compose(String name, Map lookup) { - - name = name.replaceAll(",", "|").trim(); - Object composedFunction = null; - - if (lookup.containsKey(name)) { - composedFunction = lookup.get(name); - } - else if (name.equals("") && lookup.size() >= 1 && lookup.size() <= 2) { // we may have RoutingFunction function - String functionName = lookup.keySet().stream() - .filter(fName -> !fName.equals(RoutingFunction.FUNCTION_NAME)) - .findFirst().orElseGet(() -> null); - composedFunction = lookup.get(functionName); - } - else { - String[] stages = StringUtils.delimitedListToStringArray(name, "|"); - - AtomicBoolean supplierPresent = new AtomicBoolean(); - List> composableFunctions = Stream.of(stages) - .map(funcName -> find(funcName, supplierPresent.get())) - .filter(x -> x != null) - .peek(f -> supplierPresent.set(f.getTarget() instanceof Supplier)) - .collect(Collectors.toList()); - FunctionRegistration composedRegistration = composableFunctions - .stream().reduce((a, z) -> composeFunctions(a, z)) - .orElseGet(() -> null); - - if (composedRegistration != null - && composedRegistration.getTarget() != null - && !this.types.containsKey(name)) { - - composedFunction = composedRegistration.getTarget(); - this.addType(name, composedRegistration.getType()); - this.addName(composedFunction, name); - if (composedFunction instanceof Function || composedFunction instanceof Consumer) { - this.addFunction(name, (Function) composedFunction); - } - else if (composedFunction instanceof Supplier) { - this.addSupplier(name, (Supplier) composedFunction); - } - } - - } - - return composedFunction; - } - - private FunctionRegistration find(String name, boolean supplierFound) { - Object result = this.functions.get(name); - if (result == null && !StringUtils.hasText(name)) { - if (supplierFound && this.getFunctionNames().size() == 1) { - result = this.functions.get(this.getFunctionNames().iterator().next()); - } - else if (!supplierFound && this.getSupplierNames().size() == 1) { - result = this.functions.get(this.getSupplierNames().iterator().next()); - } - } - - return getRegistration(result); - } - - @SuppressWarnings("unchecked") - private FunctionRegistration composeFunctions(FunctionRegistration aReg, - FunctionRegistration bReg) { - FunctionType aType = aReg.getType(); - FunctionType bType = bReg.getType(); - Object a = aReg.getTarget(); - Object b = bReg.getTarget(); - if (aType != null && bType != null) { - if (aType.isMessage() && !bType.isMessage()) { - bType = bType.message(); - b = message(b); - } - } - Object composedFunction = null; -// if (a instanceof Supplier && b instanceof Function) { -// Supplier> supplier = (Supplier>) a; -// if (b instanceof FluxConsumer) { -// if (supplier instanceof FluxSupplier) { -// FluxConsumer fConsumer = ((FluxConsumer) b); -// composedFunction = (Supplier>) () -> Mono.from( -// supplier.get().compose(v -> fConsumer.apply(supplier.get()))); -// } -// else { -// throw new IllegalStateException( -// "The provided supplier is finite (i.e., already composed with Consumer) " -// + "therefore it can not be composed with another consumer"); -// } -// } -// else { -// Function function = (Function) b; -// composedFunction = (Supplier) () -> function -// .apply(supplier.get()); -// } -// } -// else - if (a instanceof Function && b instanceof Function) { - Function function1 = (Function) a; - Function function2 = (Function) b; - if (function1 instanceof FluxToMonoFunction) { - if (function2 instanceof MonoToFluxFunction) { - composedFunction = function1.andThen(function2); - } - else { - throw new IllegalStateException( - "The provided function is finite (i.e., returns Mono) " - + "therefore it can *only* be composed with compatible function (i.e., Function"); - } - } - else if (function2 instanceof FluxToMonoFunction) { - composedFunction = new FluxToMonoFunction( - ((Function, Flux>) a).andThen( - ((FluxToMonoFunction) b).getTarget())); - } - else { - composedFunction = function1.andThen(function2); - } - } - else if (a instanceof Function && b instanceof Consumer) { - Function function = (Function) a; - Consumer consumer = (Consumer) b; - composedFunction = (Consumer) v -> consumer.accept(function.apply(v)); - } - else { - throw new IllegalArgumentException(String - .format("Could not compose %s and %s", a.getClass(), b.getClass())); - } - String name = aReg.getNames().iterator().next() + "|" - + bReg.getNames().iterator().next(); - return new FunctionRegistration<>(composedFunction, name) - .type(FunctionType.compose(aType, bType)); - } - - private Object message(Object input) { - if (input instanceof Supplier) { - return new MessageSupplier((Supplier) input); - } - if (input instanceof Consumer) { - return new MessageConsumer((Consumer) input); - } - if (input instanceof Function) { - return new MessageFunction((Function) input); - } - return input; - } - - private Object doLookup(Class type, String name) { - Object function = this.compose(name, this.functions); - if (function != null && type != null && !type.isAssignableFrom(function.getClass())) { - function = null; - } - return function; - } - -} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java index 83f1867dc..7fe994759 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java @@ -17,9 +17,14 @@ package org.springframework.cloud.function.context.catalog; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -34,11 +39,16 @@ import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistry; +import org.springframework.cloud.function.context.FunctionType; +import org.springframework.cloud.function.context.config.FunctionContextUtils; +import org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration; import org.springframework.cloud.function.core.FunctionInvocationHelper; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.KotlinDetector; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; @@ -46,7 +56,7 @@ import org.springframework.util.StringUtils; /** - * Implementation of {@link FunctionRegistry} capable of discovering functioins in {@link BeanFactory}. + * Implementation of {@link FunctionRegistry} capable of discovering functions in {@link BeanFactory}. * * @author Oleg Zhurakousky */ @@ -82,6 +92,18 @@ public int size() { @Override public Set getNames(Class type) { Set registeredNames = super.getNames(type); + + //--- see https://github.com/spring-cloud/spring-cloud-function/issues/947 + Set arroundWrapperNeamnames = this.applicationContext.getBeansOfType(FunctionAroundWrapper.class).keySet(); + String[] names = this.applicationContext.getBeanNamesForType(BiFunction.class); + List biFunctions = new ArrayList<>(); + for (int i = 0; i < names.length; i++) { + if (!arroundWrapperNeamnames.contains(names[i])) { + biFunctions.add(names[i]); + } + } + /// + if (type == null) { registeredNames .addAll(Arrays.asList(this.applicationContext.getBeanNamesForType(Function.class))); @@ -89,6 +111,10 @@ public Set getNames(Class type) { .addAll(Arrays.asList(this.applicationContext.getBeanNamesForType(Supplier.class))); registeredNames .addAll(Arrays.asList(this.applicationContext.getBeanNamesForType(Consumer.class))); + registeredNames + .addAll(biFunctions); + registeredNames + .addAll(Arrays.asList(this.applicationContext.getBeanNamesForType(BiConsumer.class))); registeredNames .addAll(Arrays.asList(this.applicationContext.getBeanNamesForType(FunctionRegistration.class))); } @@ -104,63 +130,113 @@ public T lookup(Class type, String functionDefinition, String... expected functionDefinition = StringUtils.hasText(functionDefinition) ? functionDefinition : this.applicationContext.getEnvironment().getProperty(FunctionProperties.FUNCTION_DEFINITION, ""); - - functionDefinition = this.normalizeFunctionDefinition(functionDefinition); + if (!this.applicationContext.containsBean(functionDefinition) || !KotlinDetector.isKotlinType(this.applicationContext.getBean(functionDefinition).getClass())) { + functionDefinition = this.normalizeFunctionDefinition(functionDefinition); + } if (!StringUtils.hasText(functionDefinition)) { logger.info("Can't determine default function definition. Please " + "use 'spring.cloud.function.definition' property to explicitly define it."); return null; } + if (!isFunctionDefinitionEligible(functionDefinition)) { + return null; + } FunctionInvocationWrapper function = this.doLookup(type, functionDefinition, expectedOutputMimeTypes); - - if (function == null) { - Set functionRegistratioinNames = super.getNames(null); - String[] functionNames = StringUtils.delimitedListToStringArray(functionDefinition.replaceAll(",", "|").trim(), "|"); - for (String functionName : functionNames) { - if (functionRegistratioinNames.contains(functionName) && logger.isDebugEnabled()) { - logger.debug("Skipping function '" + functionName + "' since it is already present"); - } - else { - Object functionCandidate = this.discoverFunctionInBeanFactory(functionName); - if (functionCandidate != null) { - Type functionType = null; - FunctionRegistration functionRegistration = null; - if (functionCandidate instanceof FunctionRegistration) { - functionRegistration = (FunctionRegistration) functionCandidate; - } - else if (this.isFunctionPojo(functionCandidate, functionName)) { - Method functionalMethod = FunctionTypeUtils.discoverFunctionalMethod(functionCandidate.getClass()); - functionCandidate = this.proxyTarget(functionCandidate, functionalMethod); - functionType = FunctionTypeUtils.fromFunctionMethod(functionalMethod); - } - else if (this.isSpecialFunctionRegistration(functionNames, functionName)) { - functionRegistration = this.applicationContext - .getBean(functionName + FunctionRegistration.REGISTRATION_NAME_SUFFIX, FunctionRegistration.class); - } - else { - functionType = FunctionTypeUtils.discoverFunctionType(functionCandidate, functionName, this.applicationContext); - } - if (functionRegistration == null) { - functionRegistration = new FunctionRegistration(functionCandidate, functionName).type(functionType); - } - // Certain Kafka Streams functions such as KStream[] return types could be null (esp when using Kotlin). - if (functionRegistration != null) { - this.register(functionRegistration); - } + Object syncInstance = functionDefinition == null ? this : functionDefinition; + synchronized (syncInstance) { + if (function == null) { + Set functionRegistratioinNames = super.getNames(null); + String[] functionNames = StringUtils.delimitedListToStringArray(functionDefinition.replaceAll(",", "|").trim(), "|"); + for (String functionName : functionNames) { + if (functionRegistratioinNames.contains(functionName) && logger.isDebugEnabled()) { + logger.debug("Skipping function '" + functionName + "' since it is already present"); } else { - if (logger.isDebugEnabled()) { - logger.debug("Function '" + functionName + "' is not available in FunctionCatalog or BeanFactory"); + Object functionCandidate = this.discoverFunctionInBeanFactory(functionName); + if (functionCandidate != null) { + Type functionType = null; + FunctionRegistration functionRegistration = null; + if (functionCandidate instanceof FunctionRegistration) { + functionRegistration = (FunctionRegistration) functionCandidate; + } + else if (functionCandidate instanceof BiFunction || functionCandidate instanceof BiConsumer) { + functionRegistration = this.registerMessagingBiFunction(functionCandidate, functionName); + } + else if (KotlinDetector.isKotlinType(functionCandidate.getClass())) { + KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper wrapper = + new KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper(functionCandidate); + wrapper.setName(functionName); + wrapper.setBeanFactory(this.applicationContext.getBeanFactory()); + functionRegistration = wrapper.getFunctionRegistration(); + } + else if (this.isFunctionPojo(functionCandidate, functionName)) { + Method functionalMethod = FunctionTypeUtils.discoverFunctionalMethod(functionCandidate.getClass()); + functionCandidate = this.proxyTarget(functionCandidate, functionalMethod); + functionType = FunctionTypeUtils.fromFunctionMethod(functionalMethod); + } + else if (this.isSpecialFunctionRegistration(functionNames, functionName)) { + functionRegistration = this.applicationContext + .getBean(functionName + FunctionRegistration.REGISTRATION_NAME_SUFFIX, FunctionRegistration.class); + } + else { + functionType = FunctionTypeUtils.discoverFunctionType(functionCandidate, functionName, this.applicationContext); + } + if (functionRegistration == null) { + functionRegistration = new FunctionRegistration(functionCandidate, functionName).type(functionType); + } + // Certain Kafka Streams functions such as KStream[] return types could be null (esp when using Kotlin). + this.register(functionRegistration); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Function '" + functionName + "' is not available in FunctionCatalog or BeanFactory"); + } } } } + function = super.doLookup(type, functionDefinition, expectedOutputMimeTypes); } - function = super.doLookup(type, functionDefinition, expectedOutputMimeTypes); } return (T) function; } + @SuppressWarnings({ "rawtypes", "unchecked" }) + private FunctionRegistration registerMessagingBiFunction(Object userFunction, String functionName) { + Type biFunctionType = FunctionContextUtils.findType(this.applicationContext.getBeanFactory(), functionName); + Type inputType1 = Object.class; + Type inputType2 = Object.class; + if (biFunctionType instanceof ParameterizedType) { + inputType1 = ((ParameterizedType) biFunctionType).getActualTypeArguments()[0]; + inputType2 = ((ParameterizedType) biFunctionType).getActualTypeArguments()[1]; + } + + if (!FunctionTypeUtils.isTypeMap(inputType2)) { + logger.debug("BiFunction's second argument must be assignable to Map, since BiFunction " + + "represents parsed Message with first argument being payload and second headers. " + + "Other signatures are not supported at the moment."); + } + + ResolvableType messageType = ResolvableType.forClassWithGenerics(Message.class, ResolvableType.forType(inputType1)); + Type biFunctionWrapperType = ResolvableType.forClassWithGenerics(Function.class, messageType, ResolvableType.forType(inputType2)).getType(); + + Function wrapperFunction = message -> { + Object payload = ((Message) message).getPayload(); + if (payload.getClass().getName().equals("org.springframework.kafka.support.KafkaNull")) { + payload = null; + } + if (userFunction instanceof BiConsumer) { + ((BiConsumer) userFunction).accept(payload, ((Message) message).getHeaders()); + return null; + } + else { + return ((BiFunction) userFunction).apply(payload, ((Message) message).getHeaders()); + } + }; + + return new FunctionRegistration<>(wrapperFunction, functionName).type(FunctionType.of(biFunctionWrapperType)); + } + private Object discoverFunctionInBeanFactory(String functionName) { Object functionCandidate = null; if (this.applicationContext.containsBean(functionName)) { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionAroundWrapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionAroundWrapper.java index 49ff7454a..bcf6c1cec 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionAroundWrapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionAroundWrapper.java @@ -40,7 +40,7 @@ public final Object apply(Object input, FunctionInvocationWrapper targetFunction String functionalTracingEnabledStr = System.getProperty("spring.sleuth.function.enabled"); boolean functionalTracingEnabled = StringUtils.hasText(functionalTracingEnabledStr) ? Boolean.parseBoolean(functionalTracingEnabledStr) : true; - if (functionalTracingEnabled) { + if (functionalTracingEnabled && input instanceof Message) { boolean isSkipOutputConversion = targetFunction.isSkipOutputConversion(); targetFunction.setSkipOutputConversion(true); try { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java index be0b9264e..ff58b449a 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtils.java @@ -31,6 +31,9 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import com.fasterxml.jackson.databind.JsonNode; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; import net.jodah.typetools.TypeResolver; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -45,6 +48,7 @@ import org.springframework.cloud.function.context.config.FunctionContextUtils; import org.springframework.cloud.function.context.config.RoutingFunction; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.KotlinDetector; import org.springframework.core.ResolvableType; import org.springframework.messaging.Message; import org.springframework.util.Assert; @@ -52,6 +56,8 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; + + /** * Set of utility operations to interrogate function definitions. * @@ -82,13 +88,26 @@ public static boolean isTypeCollection(Type type) { } type = getGenericType(type); Class rawType = type instanceof ParameterizedType ? getRawType(type) : (Class) type; - return Collection.class.isAssignableFrom(rawType); + return Collection.class.isAssignableFrom(rawType) || JsonNode.class.isAssignableFrom(rawType); + } + + public static boolean isTypeMap(Type type) { + if (Map.class.isAssignableFrom(getRawType(type))) { + return true; + } + type = getGenericType(type); + Class rawType = type instanceof ParameterizedType ? getRawType(type) : (Class) type; + return Map.class.isAssignableFrom(rawType); } public static boolean isTypeArray(Type type) { return getRawType(type).isArray(); } + public static boolean isJsonNode(Type type) { + return getRawType(type).isArray(); + } + /** * A convenience method identical to {@link #getImmediateGenericType(Type, int)} * for cases when provided 'type' is {@link Publisher} or {@link Message}. @@ -155,8 +174,14 @@ else if (Function.class.isAssignableFrom(pojoFunctionClass) || BiFunction.class. @SuppressWarnings("unchecked") public static Type discoverFunctionTypeFromClass(Class functionalClass) { - Assert.isTrue(isFunctional(functionalClass), "Type must be one of Supplier, Function or Consumer"); - + if (KotlinDetector.isKotlinPresent()) { + if (Function1.class.isAssignableFrom(functionalClass)) { + return TypeResolver.reify(Function1.class, (Class>) functionalClass); + } + else if (Function0.class.isAssignableFrom(functionalClass)) { + return TypeResolver.reify(Function0.class, (Class>) functionalClass); + } + } if (Function.class.isAssignableFrom(functionalClass)) { for (Type superInterface : functionalClass.getGenericInterfaces()) { if (superInterface != null && !superInterface.equals(Object.class)) { @@ -173,7 +198,7 @@ else if (Consumer.class.isAssignableFrom(functionalClass)) { else if (Supplier.class.isAssignableFrom(functionalClass)) { return TypeResolver.reify(Supplier.class, (Class>) functionalClass); } - return null; + return TypeResolver.reify(functionalClass); } public static Type discoverFunctionTypeFromFunctionMethod(Method functionMethod) { @@ -345,10 +370,15 @@ public static boolean isMessage(Type type) { type = getImmediateGenericType(type, 0); } - if (type instanceof ParameterizedType && !Message.class.isAssignableFrom(TypeResolver.resolveRawClass(type, null))) { + Class resolveRawClass = FunctionTypeUtils.getRawType(type); + if (type instanceof ParameterizedType && !Message.class.isAssignableFrom(resolveRawClass)) { type = getImmediateGenericType(type, 0); } - return Message.class.isAssignableFrom(TypeResolver.resolveRawClass(type, null)); + resolveRawClass = FunctionTypeUtils.getRawType(type); + if (resolveRawClass == null) { + return false; + } + return Message.class.isAssignableFrom(resolveRawClass); } /** diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java index dbfe13e49..d90d92ee5 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistry.java @@ -16,7 +16,6 @@ package org.springframework.cloud.function.context.catalog; -import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; @@ -69,7 +68,6 @@ import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -80,6 +78,7 @@ * such as type conversion, composition, POJO etc. * * @author Oleg Zhurakousky + * @author Roman Samarev * */ public class SimpleFunctionRegistry implements FunctionRegistry, FunctionInspector { @@ -89,8 +88,6 @@ public class SimpleFunctionRegistry implements FunctionRegistry, FunctionInspect * */ - private final Field headersField; - private final Set> functionRegistrations = new CopyOnWriteArraySet<>(); private final Map wrappedFunctionDefinitions = new HashMap<>(); @@ -116,8 +113,6 @@ public SimpleFunctionRegistry(ConversionService conversionService, CompositeMess this.conversionService = conversionService; this.jsonMapper = jsonMapper; this.messageConverter = messageConverter; - this.headersField = ReflectionUtils.findField(MessageHeaders.class, "headers"); - this.headersField.setAccessible(true); this.functionInvocationHelper = functionInvocationHelper; this.functionProperties = functionProperties; } @@ -149,12 +144,12 @@ public SimpleFunctionRegistry(ConversionService conversionService, CompositeMess public T lookup(Class type, String functionDefinition, String... expectedOutputMimeTypes) { functionDefinition = this.normalizeFunctionDefinition(functionDefinition); FunctionInvocationWrapper function = this.doLookup(type, functionDefinition, expectedOutputMimeTypes); - if (logger.isInfoEnabled()) { + if (logger.isDebugEnabled()) { if (function != null) { - logger.info("Located function: " + function); + logger.debug("Located function: " + function); } else { - logger.info("Failed to locate function: " + functionDefinition); + logger.debug("Failed to locate function: " + functionDefinition); } } return (T) function; @@ -162,6 +157,9 @@ public T lookup(Class type, String functionDefinition, String... expected @Override public void register(FunctionRegistration registration) { + if (!isRegistrationEligible(registration)) { + return; + } Assert.notNull(registration, "'registration' must not be null"); if (logger.isDebugEnabled()) { logger.debug("Registering function " + registration.getNames()); @@ -169,6 +167,32 @@ public void register(FunctionRegistration registration) { this.functionRegistrations.add(registration); } + @SuppressWarnings("rawtypes") + private boolean isRegistrationEligible(FunctionRegistration registration) { + if (this.functionProperties != null) { + for (String definition : this.functionProperties.getIneligibleDefinitions()) { + if (registration.getTarget().getClass().getName().equals(definition)) { + return false; + } + else if (registration.getNames().contains(definition) || registration.getTarget().getClass().getName().contains(definition)) { + return false; + } + } + } + return true; + } + + boolean isFunctionDefinitionEligible(String functionDefinition) { + if (this.functionProperties != null) { + for (String definition : this.functionProperties.getIneligibleDefinitions()) { + if (functionDefinition.contains(definition)) { + return false; + } + } + } + return true; + } + //----- @Override @@ -198,15 +222,17 @@ T doLookup(Class type, String functionDefinition, String[] expectedOutput function = this.compose(type, functionDefinition); } - if (function != null && !ObjectUtils.isEmpty(expectedOutputMimeTypes)) { - function.expectedOutputContentType = expectedOutputMimeTypes; + if (function != null) { + if (!ObjectUtils.isEmpty(expectedOutputMimeTypes)) { + function.expectedOutputContentType = expectedOutputMimeTypes; + } } else if (logger.isDebugEnabled()) { logger.debug("Function '" + functionDefinition + "' is not found in cache"); } if (function != null) { - function = this.wrapInAroundAviceIfNecessary(function); + function = this.wrapInAroundAdviceIfNecessary(function); } return (T) function; @@ -224,9 +250,11 @@ String normalizeFunctionDefinition(String functionDefinition) { ? functionDefinition.replaceAll(",", "|") : System.getProperty(FunctionProperties.FUNCTION_DEFINITION, ""); - if (!this.getNames(null).contains(functionDefinition)) { - List eligibleFunction = this.getNames(null).stream() + Set names = this.getNames(null); + if (!names.contains(functionDefinition)) { + List eligibleFunction = names.stream() .filter(name -> !RoutingFunction.FUNCTION_NAME.equals(name)) + .filter(name -> !RoutingFunction.DEFAULT_ROUTE_HANDLER.equals(name)) .collect(Collectors.toList()); if (eligibleFunction.size() == 1 && !eligibleFunction.get(0).equals(functionDefinition) @@ -243,9 +271,10 @@ String normalizeFunctionDefinition(String functionDefinition) { * There is no current use cases in functions where it is used. * The approach may change in the future. */ - private FunctionInvocationWrapper wrapInAroundAviceIfNecessary(FunctionInvocationWrapper function) { + private FunctionInvocationWrapper wrapInAroundAdviceIfNecessary(FunctionInvocationWrapper function) { FunctionInvocationWrapper wrappedFunction = function; - if (function != null && this.functionAroundWrapper != null) { + if (function != null && this.functionAroundWrapper != null && !function.isSupplier() + && !function.isInputTypePublisher() && !function.isOutputTypePublisher() && !FunctionTypeUtils.isCollectionOfMessage(function.getOutputType())) { wrappedFunction = new FunctionInvocationWrapper(function) { @Override Object doApply(Object input) { @@ -285,7 +314,7 @@ private FunctionInvocationWrapper findFunctionInFunctionRegistrations(String fun /* * */ - private synchronized FunctionInvocationWrapper compose(Class type, String functionDefinition) { + private FunctionInvocationWrapper compose(Class type, String functionDefinition) { String[] functionNames = StringUtils.delimitedListToStringArray(functionDefinition.replaceAll(",", "|").trim(), "|"); FunctionInvocationWrapper composedFunction = null; @@ -407,6 +436,8 @@ public class FunctionInvocationWrapper implements Function, Cons private boolean isSingleton = true; + private boolean propagateInputHeaders; + /* * This is primarily to support Stream's ability to access * un-converted payload (e.g., to evaluate expression on some attribute of a payload) @@ -432,6 +463,15 @@ public class FunctionInvocationWrapper implements Function, Cons this.outputType = this.normalizeType(outputType); this.functionDefinition = functionDefinition; this.message = this.inputType != null && FunctionTypeUtils.isMessage(this.inputType); + if (functionProperties != null) { + Map funcConfiguration = functionProperties.getConfiguration(); + if (!CollectionUtils.isEmpty(funcConfiguration)) { + FunctionConfigurationProperties configuration = funcConfiguration.get(functionDefinition); + if (configuration != null) { + propagateInputHeaders = configuration.isCopyInputHeaders(); + } + } + } } public boolean isSkipOutputConversion() { @@ -547,7 +587,7 @@ public Object apply(Object input) { if (logger.isDebugEnabled() && !(input instanceof Publisher)) { logger.debug("Invoking function " + this); } - Object result = this.doApply(input); + Object result = (this.getTarget() instanceof PassThruFunction) ? input : this.doApply(input); if (result != null && this.outputType != null) { result = this.convertOutputIfNecessary(result, this.outputType, this.expectedOutputContentType); @@ -625,9 +665,9 @@ public Function andThen(Function aft Type composedFunctionType; if (afterWrapper.outputType == null) { - composedFunctionType = ResolvableType.forClassWithGenerics(Consumer.class, this.inputType == null - ? null - : ResolvableType.forType(this.inputType)).getType(); + composedFunctionType = (this.inputType == null) ? + ResolvableType.forClassWithGenerics(Supplier.class, ResolvableType.forType(Object.class)).getType() : + ResolvableType.forClassWithGenerics(Consumer.class, ResolvableType.forType(this.inputType)).getType(); } else if (this.inputType == null && afterWrapper.outputType != null) { ResolvableType composedOutputType; @@ -738,18 +778,12 @@ private Class getRawClassFor(@Nullable Type type) { /** * Will wrap the result in a Message if necessary and will copy input headers to the output message. */ - @SuppressWarnings("unchecked") private Object enrichInvocationResultIfNecessary(Object input, Object result) { if (result != null && !(result instanceof Publisher) && input instanceof Message) { if (result instanceof Message) { if (functionInvocationHelper != null && CloudEventMessageUtils.isCloudEvent(((Message) input))) { result = functionInvocationHelper.postProcessResult(result, (Message) input); } - else { - Map headersMap = (Map) ReflectionUtils - .getField(SimpleFunctionRegistry.this.headersField, ((Message) result).getHeaders()); - this.sanitizeHeaders(((Message) input).getHeaders()).forEach((k, v) -> headersMap.putIfAbsent(k, v)); - } } else { if (functionInvocationHelper != null && CloudEventMessageUtils.isCloudEvent(((Message) input))) { @@ -781,6 +815,9 @@ private Map sanitizeHeaders(MessageHeaders headers) { */ @SuppressWarnings("unchecked") private Object fluxifyInputIfNecessary(Object input) { + if (input instanceof Message && !((Message) input).getHeaders().containsKey("user-agent") && this.isConsumer() && !this.isInputTypePublisher()) { + return input; + } if (FunctionTypeUtils.isMultipleArgumentType(this.inputType)) { return input; } @@ -893,6 +930,11 @@ else if (value instanceof Mono) { if (inputValue instanceof Message && !this.isInputTypeMessage()) { inputValue = ((Message) inputValue).getPayload(); } + + if (logger.isDebugEnabled()) { + logger.debug("Invoking function: " + this + "with input type: " + this.getInputType()); + } + Object result = ((Function) this.target).apply(inputValue); if (result instanceof Publisher && functionInvocationHelper != null) { @@ -1063,11 +1105,12 @@ else if (input instanceof Message) { : convertedInput; } if (convertedInput != null && logger.isDebugEnabled()) { - logger.debug("Converted Message: " + input + " to: " + convertedInput); + logger.debug("Converted Message: " + input + " to: " + + (convertedInput instanceof OriginalMessageHolder ? ((OriginalMessageHolder) convertedInput).value.getClass() : convertedInput)); } } else { - convertedInput = this.convertNonMessageInputIfNecessary(type, input, JsonMapper.isJsonString(input) || input instanceof Map); + convertedInput = this.convertNonMessageInputIfNecessary(type, input, JsonMapper.isJsonString(input)); if (convertedInput != null && logger.isDebugEnabled()) { logger.debug("Converted input: " + input + " to: " + convertedInput); } @@ -1090,6 +1133,9 @@ private Message filterOutHeaders(Message message) { } private boolean isExtractPayload(Message message, Type type) { + if (this.propagateInputHeaders) { + return false; + } if (this.isRoutingFunction()) { return false; } @@ -1121,43 +1167,55 @@ private boolean isExtractPayload(Message message, Type type) { * This is an optional conversion which would only happen if `expected-content-type` is * set as a header in a message or explicitly provided as part of the lookup. */ + @SuppressWarnings("unchecked") private Object convertOutputIfNecessary(Object output, Type type, String[] contentType) { - if (output instanceof Message && ((Message) output).getPayload() instanceof byte[]) { - return output; - } + Object convertedOutput = output; if (this.skipOutputConversion) { - return output; + return convertedOutput; } - if (functionAroundWrapper == null && output instanceof Message && isExtractPayload((Message) output, type)) { - output = ((Message) output).getPayload(); + + if (convertedOutput instanceof Publisher) { + return this.convertOutputPublisherIfNecessary((Publisher) convertedOutput, type, contentType); } - if (!(output instanceof Publisher) && this.enhancer != null) { - output = enhancer.apply(output); + + if (convertedOutput instanceof Message) { + if (((Message) convertedOutput).getPayload() instanceof byte[]) { + return convertedOutput; + } + else if (isExtractPayload((Message) convertedOutput, type)) { + convertedOutput = ((Message) convertedOutput).getPayload(); + } } - if (ObjectUtils.isEmpty(contentType) && !(output instanceof Publisher)) { - return output; + if (this.enhancer != null) { + convertedOutput = enhancer.apply(convertedOutput); + } + if (this.getTarget() instanceof PassThruFunction) { // scst-2303 + Message enrichedMessage = MessageBuilder.fromMessage((Message) convertedOutput) + .setHeader(MessageHeaders.CONTENT_TYPE, contentType[0]).build(); + return messageConverter.toMessage(enrichedMessage.getPayload(), enrichedMessage.getHeaders()); + } + + if (ObjectUtils.isEmpty(contentType)) { + return convertedOutput; } - Object convertedOutput = output; + if (FunctionTypeUtils.isMultipleArgumentType(type)) { convertedOutput = this.convertMultipleOutputArgumentTypeIfNecesary(convertedOutput, type, contentType); } - else if (output instanceof Publisher) { - convertedOutput = this.convertOutputPublisherIfNecessary((Publisher) output, type, contentType); - } - else if (output instanceof Message) { - convertedOutput = this.convertOutputMessageIfNecessary(output, ObjectUtils.isEmpty(contentType) ? null : contentType[0]); + else if (convertedOutput instanceof Message) { + convertedOutput = this.convertOutputMessageIfNecessary(convertedOutput, ObjectUtils.isEmpty(contentType) ? null : contentType[0]); } - else if (output instanceof Collection && this.isOutputTypeMessage()) { - convertedOutput = this.convertMultipleOutputValuesIfNecessary(output, ObjectUtils.isEmpty(contentType) ? null : contentType); + else if (convertedOutput instanceof Collection && this.isOutputTypeMessage()) { + convertedOutput = this.convertMultipleOutputValuesIfNecessary(convertedOutput, ObjectUtils.isEmpty(contentType) ? null : contentType); } - else if (ObjectUtils.isArray(output) && !(output instanceof byte[])) { - convertedOutput = this.convertMultipleOutputValuesIfNecessary(output, ObjectUtils.isEmpty(contentType) ? null : contentType); + else if (ObjectUtils.isArray(convertedOutput) && !(convertedOutput instanceof byte[])) { + convertedOutput = this.convertMultipleOutputValuesIfNecessary(convertedOutput, ObjectUtils.isEmpty(contentType) ? null : contentType); } else { - convertedOutput = messageConverter.toMessage(output, + convertedOutput = messageConverter.toMessage(convertedOutput, new MessageHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, contentType == null ? "application/json" : contentType[0]))); } @@ -1280,6 +1338,9 @@ private Object convertInputMessageIfNecessary(Message message, Type type) { ? SimpleFunctionRegistry.this.messageConverter.fromMessage(message, rawType, itemType) : SimpleFunctionRegistry.this.messageConverter.fromMessage(message, rawType); + if (convertedInput != null && !rawType.isAssignableFrom(convertedInput.getClass())) { + logger.warn("Failed to convert input to " + rawType + ". Will attempt to invoke function with raw type"); + } if (FunctionTypeUtils.isMessage(type)) { if (convertedInput == null) { @@ -1327,17 +1388,17 @@ private Object convertMultipleOutputArgumentTypeIfNecesary(Object output, Type t */ @SuppressWarnings("unchecked") private Object convertOutputMessageIfNecessary(Object output, String expectedOutputContetntType) { - Map headersMap = (Map) ReflectionUtils - .getField(SimpleFunctionRegistry.this.headersField, ((Message) output).getHeaders()); String contentType = ((Message) output).getHeaders().containsKey(FunctionProperties.EXPECT_CONTENT_TYPE_HEADER) ? (String) ((Message) output).getHeaders().get(FunctionProperties.EXPECT_CONTENT_TYPE_HEADER) : expectedOutputContetntType; if (StringUtils.hasText(contentType)) { + Map headersMap = new HashMap(((Message) output).getHeaders()); String[] expectedContentTypes = StringUtils.delimitedListToStringArray(contentType, ","); for (String expectedContentType : expectedContentTypes) { headersMap.put(MessageHeaders.CONTENT_TYPE, expectedContentType); - Object result = messageConverter.toMessage(((Message) output).getPayload(), ((Message) output).getHeaders()); + Message message = MessageBuilder.withPayload(((Message) output).getPayload()).copyHeaders(headersMap).build(); + Object result = messageConverter.toMessage(message.getPayload(), message.getHeaders()); if (result != null) { return result; } @@ -1377,10 +1438,22 @@ else if (FunctionTypeUtils.isFlux(type) && publisher instanceof Mono) { ? FunctionTypeUtils.getImmediateGenericType(type, 0) : type; return publisher instanceof Mono - ? Mono.from(publisher).map(v -> this.convertInputIfNecessary(v, actualType == null ? type : actualType)) - .doOnError(ex -> logger.error("Failed to convert input", (Throwable) ex)) - : Flux.from(publisher).map(v -> this.convertInputIfNecessary(v, actualType == null ? type : actualType)) - .doOnError(ex -> logger.error("Failed to convert input", (Throwable) ex)); + ? Mono.from(publisher).map(v -> { + try { + return this.convertInputIfNecessary(v, actualType == null ? type : actualType); + } + catch (Exception e) { + throw new IllegalStateException("Failed to convert input", e); + } + }) + : Flux.from(publisher).map(v -> { + try { + return this.convertInputIfNecessary(v, actualType == null ? type : actualType); + } + catch (Exception e) { + throw new IllegalStateException("Failed to convert input", e); + } + }); } /* @@ -1389,10 +1462,22 @@ else if (FunctionTypeUtils.isFlux(type) && publisher instanceof Mono) { @SuppressWarnings("unchecked") private Object convertOutputPublisherIfNecessary(Publisher publisher, Type type, String[] expectedOutputContentType) { return publisher instanceof Mono - ? Mono.from(publisher).map(v -> this.convertOutputIfNecessary(v, type, expectedOutputContentType)) - .doOnError(ex -> logger.error("Failed to convert output", (Throwable) ex)) - : Flux.from(publisher).map(v -> this.convertOutputIfNecessary(v, type, expectedOutputContentType)) - .doOnError(ex -> logger.error("Failed to convert output", (Throwable) ex)); + ? Mono.from(publisher).map(v -> { + try { + return this.convertOutputIfNecessary(v, type, expectedOutputContentType); + } + catch (Exception e) { + throw new IllegalStateException("Failed to convert output", e); + } + }) + : Flux.from(publisher).map(v -> { + try { + return this.convertOutputIfNecessary(v, type, expectedOutputContentType); + } + catch (Exception e) { + throw new IllegalStateException("Failed to convert output", e); + } + }); } } @@ -1417,4 +1502,11 @@ public Message getOriginalMessage() { return this.originalMessage; } } + + public static class PassThruFunction implements Function { + @Override + public Object apply(Object t) { + return t; + } + } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java index 4d3a7eddc..934e6991e 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 the original author or authors. + * Copyright 2016-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,12 +35,15 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.function.cloudevent.CloudEventsFunctionInvocationHelper; +import org.springframework.cloud.function.context.DefaultMessageRoutingHandler; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionProperties; +import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.MessageRoutingCallback; import org.springframework.cloud.function.context.catalog.BeanFactoryAwareFunctionRegistry; import org.springframework.cloud.function.context.converter.avro.AvroSchemaMessageConverter; +import org.springframework.cloud.function.context.converter.avro.AvroSchemaServiceManager; import org.springframework.cloud.function.context.converter.avro.AvroSchemaServiceManagerImpl; import org.springframework.cloud.function.core.FunctionInvocationHelper; import org.springframework.cloud.function.json.GsonMapper; @@ -55,6 +58,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -64,11 +68,11 @@ import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.StringMessageConverter; +import org.springframework.stereotype.Component; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; - /** * @author Dave Syer * @author Mark Fisher @@ -76,6 +80,7 @@ * @author Artem Bilan * @author Anshul Mehra * @author Soby Chacko + * @author Chris Bono */ @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(FunctionCatalog.class) @@ -109,8 +114,7 @@ public FunctionRegistry functionCatalog(List messageConverters if (!CollectionUtils.isEmpty(messageConverters)) { for (MessageConverter mc : messageConverters) { if (mc instanceof CompositeMessageConverter) { - List conv = ((CompositeMessageConverter) mc).getConverters().stream() - .collect(Collectors.toList()); + List conv = ((CompositeMessageConverter) mc).getConverters().stream().collect(Collectors.toList()); mcList.addAll(conv); } else { @@ -120,7 +124,7 @@ public FunctionRegistry functionCatalog(List messageConverters } mcList = mcList.stream() - .filter(c -> isConverterEligible(c)) + .filter(this::isConverterEligible) .collect(Collectors.toList()); mcList.add(new JsonMessageConverter(jsonMapper)); @@ -128,33 +132,24 @@ public FunctionRegistry functionCatalog(List messageConverters mcList.add(new StringMessageConverter()); mcList.add(new PrimitiveTypesFromStringMessageConverter(conversionService)); - if (!CollectionUtils.isEmpty(mcList)) { - messageConverter = new SmartCompositeMessageConverter(mcList); - if (functionInvocationHelper instanceof CloudEventsFunctionInvocationHelper) { - ((CloudEventsFunctionInvocationHelper) functionInvocationHelper).setMessageConverter(messageConverter); - } + messageConverter = new SmartCompositeMessageConverter(mcList); + if (functionInvocationHelper instanceof CloudEventsFunctionInvocationHelper) { + ((CloudEventsFunctionInvocationHelper) functionInvocationHelper).setMessageConverter(messageConverter); } FunctionProperties functionProperties = context.getBean(FunctionProperties.class); return new BeanFactoryAwareFunctionRegistry(conversionService, messageConverter, jsonMapper, functionProperties, functionInvocationHelper); } - @Bean - @ConditionalOnMissingBean - @ConditionalOnClass(name = "org.apache.avro.Schema") - public MessageConverter avroSchemaMessageConverter() { - return new AvroSchemaMessageConverter(new AvroSchemaServiceManagerImpl()); - } - - @Bean - @ConditionalOnMissingBean - @ConditionalOnClass(name = "io.cloudevents.spring.messaging.CloudEventMessageConverter") - public MessageConverter cloudEventMessageConverter() { - return new CloudEventMessageConverter(); - } - + @SuppressWarnings({ "unchecked", "rawtypes" }) @Bean(RoutingFunction.FUNCTION_NAME) - RoutingFunction functionRouter(FunctionCatalog functionCatalog, FunctionProperties functionProperties, - BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) { + public RoutingFunction functionRouter(FunctionCatalog functionCatalog, FunctionProperties functionProperties, + BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback, + @Nullable DefaultMessageRoutingHandler defaultMessageRoutingHandler) { + if (defaultMessageRoutingHandler != null) { + FunctionRegistration functionRegistration = new FunctionRegistration(defaultMessageRoutingHandler, RoutingFunction.DEFAULT_ROUTE_HANDLER); + functionRegistration.type(ResolvableType.forClassWithGenerics(Consumer.class, ResolvableType.forClassWithGenerics(Message.class, Object.class)).getType()); + ((FunctionRegistry) functionCatalog).register(functionRegistration); + } return new RoutingFunction(functionCatalog, functionProperties, new BeanFactoryResolver(beanFactory), routingCallback); } @@ -163,18 +158,43 @@ private boolean isConverterEligible(Object messageConverter) { if (messageConverterName.startsWith("org.springframework.cloud.")) { return true; } - else if (!messageConverterName.startsWith("org.springframework.")) { - return true; + return !messageConverterName.startsWith("org.springframework."); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "io.cloudevents.spring.messaging.CloudEventMessageConverter") + static class CloudEventsMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean + public CloudEventMessageConverter cloudEventMessageConverter() { + return new CloudEventMessageConverter(); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(name = "org.apache.avro.Schema") + @ConditionalOnProperty(value = "spring.cloud.stream.avro.enabled", havingValue = "true", matchIfMissing = true) + static class AvroSchemaMessageConverterConfiguration { + + @Bean + @ConditionalOnMissingBean + public AvroSchemaServiceManager avroSchemaServiceManager() { + return new AvroSchemaServiceManagerImpl(); + } + + @Bean + @ConditionalOnMissingBean + public AvroSchemaMessageConverter avroSchemaMessageConverter(AvroSchemaServiceManager avroSchemaServiceManager) { + return new AvroSchemaMessageConverter(avroSchemaServiceManager); } - return false; } + @ComponentScan(basePackages = "${spring.cloud.function.scan.packages:functions}", + includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, classes = { Supplier.class, Function.class, Consumer.class }), + excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = { Configuration.class, Component.class})) @Configuration(proxyBeanMethods = false) - @ComponentScan(basePackages = "${spring.cloud.function.scan.packages:functions}", // - includeFilters = @Filter(type = FilterType.ASSIGNABLE_TYPE, - classes = {Supplier.class, Function.class, Consumer.class})) - @ConditionalOnProperty(prefix = "spring.cloud.function.scan", name = "enabled", havingValue = "true", - matchIfMissing = true) + @ConditionalOnProperty(prefix = "spring.cloud.function.scan", name = "enabled", havingValue = "true", matchIfMissing = true) protected static class PlainFunctionScanConfiguration { } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java index 31832ff54..dd8700ffd 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java @@ -176,6 +176,7 @@ && new ClassPathResource(basePackage.replace(".", "/")).exists()) { List messageConverters = new ArrayList<>(); JsonMapper jsonMapper = this.context.getBean(JsonMapper.class); + messageConverters.addAll(context.getBeansOfType(MessageConverter.class).values()); messageConverters.add(new JsonMessageConverter(jsonMapper)); messageConverters.add(new ByteArrayMessageConverter()); messageConverters.add(new StringMessageConverter()); diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java index 0f8bb5ecb..81b0ec29f 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java @@ -18,6 +18,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.security.AccessController; import java.security.PrivilegedAction; @@ -29,8 +30,6 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; -import org.springframework.cloud.function.core.FunctionFactoryMetadata; -import org.springframework.context.annotation.ScannedGenericBeanDefinition; import org.springframework.core.ResolvableType; import org.springframework.core.io.Resource; import org.springframework.core.type.MethodMetadata; @@ -61,12 +60,17 @@ else if (registry.containsBean(name)) { } } + Class beanClass = null; + + if (definition == null) { return null; } - else if (definition instanceof ScannedGenericBeanDefinition) { + + if (definition instanceof AbstractBeanDefinition) { try { - return FunctionTypeUtils.discoverFunctionTypeFromClass(definition.getBeanClass()); + beanClass = resolveBeanClass(definition); + return FunctionTypeUtils.discoverFunctionTypeFromClass(beanClass); } catch (Exception e) { // ignore since name may not be actually resolved to a class in some cases @@ -88,20 +92,10 @@ else if (source instanceof Resource) { if (type != null) { param = type.getType(); } - else { - Class beanClass = definition.hasBeanClass() ? definition.getBeanClass() : null; - if (beanClass != null - && !FunctionFactoryMetadata.class.isAssignableFrom(beanClass)) { - param = beanClass; - } - else { - Object bean = registry.getBean(actualName); - // could be FunctionFactoryMetadata. . . TODO investigate and fix - if (bean instanceof FunctionFactoryMetadata) { - param = ((FunctionFactoryMetadata) bean).getFactoryMethod().getGenericReturnType(); - } - } - } + } + + if (!(param instanceof ParameterizedType) && beanClass != null) { + return FunctionTypeUtils.discoverFunctionTypeFromClass(beanClass); } return param; } @@ -124,6 +118,15 @@ public static Class[] getParamTypesFromBeanDefinitionFactory(Class factory return params.toArray(new Class[0]); } + private static Class resolveBeanClass(AbstractBeanDefinition beanDefinition) { + try { + return beanDefinition.hasBeanClass() ? beanDefinition.getBeanClass() : ClassUtils.getDefaultClassLoader().loadClass(beanDefinition.getBeanClassName()); + } + catch (Exception e) { + throw new IllegalStateException("Can't resolve class", e); + } + } + private static Type findBeanType(AbstractBeanDefinition definition, String declaringClassName, String methodName) { Class factory = ClassUtils.resolveClassName(declaringClassName, null); Class[] params = getParamTypesFromBeanDefinitionFactory(factory, definition); diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java index 2f8b17846..3993a475b 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/JsonMessageConverter.java @@ -16,12 +16,15 @@ package org.springframework.cloud.function.context.config; +import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; -import java.util.Collection; import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils; import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -76,10 +79,10 @@ protected boolean canConvertFrom(Message message, @Nullable Class targetCl @Override protected Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { - if (targetClass.isInstance(message.getPayload()) && !(message.getPayload() instanceof Collection)) { - return message.getPayload(); + if (conversionHint instanceof ParameterizedTypeReference) { + conversionHint = ((ParameterizedTypeReference) conversionHint).getType(); } - Type convertToType = conversionHint == null ? targetClass : (Type) conversionHint; + Type convertToType = this.getResolvedType(targetClass, conversionHint); if (targetClass == byte[].class && message.getPayload() instanceof String) { return ((String) message.getPayload()).getBytes(StandardCharsets.UTF_8); } @@ -96,7 +99,13 @@ else if (logger.isDebugEnabled()) { if (payload instanceof byte[]) { payload = new String((byte[]) payload, StandardCharsets.UTF_8); } - logger.warn("Failed to convert value: " + payload, e); + + if (logger.isDebugEnabled()) { + logger.debug("Failed to convert value: " + payload + " to: " + targetClass, e); + } + else { + logger.warn("Failed to convert value: " + payload + " to: " + targetClass); + } } } } @@ -110,4 +119,22 @@ protected Object convertToInternal(Object payload, @Nullable MessageHeaders head return jsonMapper.toJson(payload); } + + private Type getResolvedType(Class targetClass, @Nullable Object conversionHint) { + if (conversionHint instanceof MethodParameter) { + MethodParameter param = (MethodParameter) conversionHint; + param = param.nestedIfOptional(); + if (Message.class.isAssignableFrom(param.getParameterType())) { + param = param.nested(); + } + Type genericParameterType = param.getNestedGenericParameterType(); + Class contextClass = param.getContainingClass(); + return GenericTypeResolver.resolveType(genericParameterType, contextClass); + } + else if (conversionHint instanceof ParameterizedType) { + return (Type) conversionHint; + } + return targetClass; + } + } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java index f9dc48484..034a6b883 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java @@ -35,20 +35,12 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.beans.factory.BeanNameAware; -import org.springframework.beans.factory.FactoryBean; -import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.config.ConstructorArgumentValues; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.cloud.function.context.FunctionRegistration; +import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ResolvableType; @@ -83,47 +75,11 @@ public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) { }; } - /** - * Will transform all discovered Kotlin's Function lambdas to java - * Supplier, Function and Consumer, retaining the original Kotlin type - * characteristics. - * - * @return the bean factory post processor - */ - @Bean - public static BeanFactoryPostProcessor kotlinToFunctionTransformer(ConfigurableListableBeanFactory beanFactory) { - return new BeanFactoryPostProcessor() { - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) - throws BeansException { - String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames(); - for (String beanDefinitionName : beanDefinitionNames) { - BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName); - - if (beanDefinition instanceof AnnotatedBeanDefinition && ((AnnotatedBeanDefinition) beanDefinition).getFactoryMethodMetadata() != null) { - String typeName = ((AnnotatedBeanDefinition) beanDefinition).getFactoryMethodMetadata().getReturnTypeName(); - if (typeName.startsWith("kotlin.jvm.functions.Function")) { - RootBeanDefinition cbd = new RootBeanDefinition(KotlinFunctionWrapper.class); - ConstructorArgumentValues ca = new ConstructorArgumentValues(); - ca.addGenericArgumentValue(beanDefinition); - cbd.setConstructorArgumentValues(ca); - ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition(beanDefinitionName + FunctionRegistration.REGISTRATION_NAME_SUFFIX, cbd); - } - } - } - } - }; - } @SuppressWarnings({ "unchecked", "rawtypes" }) public static final class KotlinFunctionWrapper implements Function, Supplier, Consumer, Function0, Function1, Function2, - Function3, Function4, - FactoryBean, - BeanNameAware, - BeanFactoryAware { - + Function3, Function4 { private final Object kotlinLambdaTarget; private String name; @@ -139,9 +95,6 @@ public Object apply(Object input) { if (ObjectUtils.isEmpty(input)) { return this.invoke(); } - else if (ObjectUtils.isArray(input)) { - return null; - } else { return this.invoke(input); } @@ -167,7 +120,10 @@ public Object invoke(Object arg0) { if (CoroutinesUtils.isValidSuspendingFunction(kotlinLambdaTarget, arg0)) { return CoroutinesUtils.invokeSuspendingFunction(kotlinLambdaTarget, arg0); } - return ((Function1) this.kotlinLambdaTarget).invoke(arg0); + if (this.kotlinLambdaTarget instanceof Function1) { + return ((Function1) this.kotlinLambdaTarget).invoke(arg0); + } + return ((Function) this.kotlinLambdaTarget).apply(arg0); } @Override @@ -175,7 +131,10 @@ public Object invoke() { if (CoroutinesUtils.isValidSuspendingSupplier(kotlinLambdaTarget)) { return CoroutinesUtils.invokeSuspendingSupplier(kotlinLambdaTarget); } - return ((Function0) this.kotlinLambdaTarget).invoke(); + if (this.kotlinLambdaTarget instanceof Function0) { + return ((Function0) this.kotlinLambdaTarget).invoke(); + } + return ((Supplier) this.kotlinLambdaTarget).get(); } @Override @@ -192,8 +151,7 @@ public Object get() { return this.apply(null); } - @Override - public FunctionRegistration getObject() throws Exception { + public FunctionRegistration getFunctionRegistration() { String name = this.name.endsWith(FunctionRegistration.REGISTRATION_NAME_SUFFIX) ? this.name.replace(FunctionRegistration.REGISTRATION_NAME_SUFFIX, "") : this.name; @@ -236,7 +194,9 @@ else if (isValidKotlinSuspendConsumer(functionType, types)) { ResolvableType.forClassWithGenerics(Flux.class, ResolvableType.forType(continuationArgType)) ).getType(); } - else { + else if (!FunctionTypeUtils.isFunction(functionType) + && !FunctionTypeUtils.isConsumer(functionType) + && !FunctionTypeUtils.isSupplier(functionType)) { throw new UnsupportedOperationException("Multi argument Kotlin functions are not currently supported"); } registration = registration.type(functionType); @@ -284,17 +244,16 @@ private boolean isTypeRepresentedByClass(Type type, Class clazz) { return type.getTypeName().contains(clazz.getName()); } - @Override public Class getObjectType() { return FunctionRegistration.class; } - @Override - public void setBeanName(String name) { + + public void setName(String name) { this.name = name; } - @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java index dd6b965b3..8a5b66569 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.context.config; +import java.util.Map; import java.util.function.Function; import org.apache.commons.logging.Log; @@ -29,16 +30,18 @@ import org.springframework.cloud.function.context.MessageRoutingCallback; import org.springframework.cloud.function.context.MessageRoutingCallback.FunctionRoutingResult; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.context.message.MessageUtils; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.DataBindingPropertyAccessor; +import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.messaging.Message; import org.springframework.util.Assert; import org.springframework.util.StringUtils; - /** * An implementation of Function which acts as a gateway/router by actually * delegating incoming invocation to a function specified .. . @@ -55,10 +58,18 @@ public class RoutingFunction implements Function { */ public static final String FUNCTION_NAME = "functionRouter"; + /** + * The name of this function for routing of un-routable messages. + */ + public static final String DEFAULT_ROUTE_HANDLER = "defaultMessageRoutingHandler"; + private static Log logger = LogFactory.getLog(RoutingFunction.class); private final StandardEvaluationContext evalContext = new StandardEvaluationContext(); + private final SimpleEvaluationContext headerEvalContext = SimpleEvaluationContext + .forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess()).build(); + private final SpelExpressionParser spelParser = new SpelExpressionParser(); private final FunctionCatalog functionCatalog; @@ -71,6 +82,11 @@ public RoutingFunction(FunctionCatalog functionCatalog, FunctionProperties funct this(functionCatalog, functionProperties, null, null); } + public RoutingFunction(FunctionCatalog functionCatalog, Map propertiesMap, + BeanResolver beanResolver, MessageRoutingCallback routingCallback) { + this(functionCatalog, extractIntoFunctionProperties(propertiesMap), beanResolver, routingCallback); + } + public RoutingFunction(FunctionCatalog functionCatalog, FunctionProperties functionProperties, BeanResolver beanResolver, MessageRoutingCallback routingCallback) { this.functionCatalog = functionCatalog; @@ -80,6 +96,13 @@ public RoutingFunction(FunctionCatalog functionCatalog, FunctionProperties funct evalContext.setBeanResolver(beanResolver); } + private static FunctionProperties extractIntoFunctionProperties(Map propertiesMap) { + FunctionProperties functionProperties = new FunctionProperties(); + functionProperties.setDefinition(propertiesMap.get(FunctionProperties.FUNCTION_DEFINITION)); + functionProperties.setRoutingExpression(propertiesMap.get(FunctionProperties.ROUTING_EXPRESSION)); + return functionProperties; + } + @Override public Object apply(Object input) { return this.route(input, input instanceof Publisher); @@ -116,14 +139,14 @@ private Object route(Object input, boolean originalInputIsPublisher) { } } if (function == null) { - if (StringUtils.hasText((String) message.getHeaders().get("spring.cloud.function.definition"))) { - function = functionFromDefinition((String) message.getHeaders().get("spring.cloud.function.definition")); + if (StringUtils.hasText((String) message.getHeaders().get(FunctionProperties.FUNCTION_DEFINITION))) { + function = functionFromDefinition((String) message.getHeaders().get(FunctionProperties.FUNCTION_DEFINITION)); if (function.isInputTypePublisher()) { this.assertOriginalInputIsNotPublisher(originalInputIsPublisher); } } - else if (StringUtils.hasText((String) message.getHeaders().get("spring.cloud.function.routing-expression"))) { - function = this.functionFromExpression((String) message.getHeaders().get("spring.cloud.function.routing-expression"), message); + else if (StringUtils.hasText((String) message.getHeaders().get(FunctionProperties.ROUTING_EXPRESSION))) { + function = this.functionFromExpression((String) message.getHeaders().get(FunctionProperties.ROUTING_EXPRESSION), message, true); if (function.isInputTypePublisher()) { this.assertOriginalInputIsNotPublisher(originalInputIsPublisher); } @@ -142,18 +165,16 @@ else if (StringUtils.hasText(functionProperties.getDefinition())) { } } else if (input instanceof Publisher) { - if (function == null) { - if (StringUtils.hasText(functionProperties.getDefinition())) { - function = functionFromDefinition(functionProperties.getDefinition()); - } - else if (StringUtils.hasText(functionProperties.getRoutingExpression())) { - function = this.functionFromExpression(functionProperties.getRoutingExpression(), input); - } - else { - return input instanceof Mono + if (StringUtils.hasText(functionProperties.getDefinition())) { + function = functionFromDefinition(functionProperties.getDefinition()); + } + else if (StringUtils.hasText(functionProperties.getRoutingExpression())) { + function = this.functionFromExpression(functionProperties.getRoutingExpression(), input); + } + else { + return input instanceof Mono ? Mono.from((Publisher) input).map(v -> route(v, originalInputIsPublisher)) : Flux.from((Publisher) input).map(v -> route(v, originalInputIsPublisher)); - } } } else { @@ -172,6 +193,12 @@ else if (StringUtils.hasText(functionProperties.getRoutingExpression())) { } } + if (function.getTarget().equals(this)) { + throw new IllegalStateException("Failed to establish route, and routing to itself is not allowed as it creates a loop. Please provide: " + + "'spring.cloud.function.definition' as Message header or as application property or " + + "'spring.cloud.function.routing-expression' as application property."); + } + return function.apply(input); } @@ -182,7 +209,7 @@ private void assertOriginalInputIsNotPublisher(boolean originalInputIsPublisher) } private FunctionInvocationWrapper functionFromDefinition(String definition) { - FunctionInvocationWrapper function = functionCatalog.lookup(definition); + FunctionInvocationWrapper function = this.resolveFunction(definition); Assert.notNull(function, "Failed to lookup function to route based on the value of 'spring.cloud.function.definition' property '" + functionProperties.getDefinition() + "'"); if (logger.isInfoEnabled()) { @@ -192,15 +219,32 @@ private FunctionInvocationWrapper functionFromDefinition(String definition) { } private FunctionInvocationWrapper functionFromExpression(String routingExpression, Object input) { + return functionFromExpression(routingExpression, input, false); + } + + private FunctionInvocationWrapper functionFromExpression(String routingExpression, Object input, boolean isViaHeader) { Expression expression = spelParser.parseExpression(routingExpression); - String functionName = expression.getValue(this.evalContext, input, String.class); - Assert.hasText(functionName, "Failed to resolve function name based on routing expression '" + functionProperties.getRoutingExpression() + "'"); - FunctionInvocationWrapper function = functionCatalog.lookup(functionName); + if (input instanceof Message) { + input = MessageUtils.toCaseInsensitiveHeadersStructure((Message) input); + } + + String definition = isViaHeader ? expression.getValue(this.headerEvalContext, input, String.class) : expression.getValue(this.evalContext, input, String.class); + Assert.hasText(definition, "Failed to resolve function name based on routing expression '" + functionProperties.getRoutingExpression() + "'"); + FunctionInvocationWrapper function = this.resolveFunction(definition); Assert.notNull(function, "Failed to lookup function to route to based on the expression '" - + functionProperties.getRoutingExpression() + "' whcih resolved to '" + functionName + "' function name."); + + functionProperties.getRoutingExpression() + "' which resolved to '" + definition + "' function definition."); if (logger.isInfoEnabled()) { logger.info("Resolved function from provided [routing-expression] " + routingExpression); } return function; } + + private FunctionInvocationWrapper resolveFunction(String definition) { + FunctionInvocationWrapper function = functionCatalog.lookup(definition); + if (function == null) { + function = functionCatalog.lookup(RoutingFunction.DEFAULT_ROUTE_HANDLER); + } + return function; + } + } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/SmartCompositeMessageConverter.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/SmartCompositeMessageConverter.java index fedbe5e98..12c3bdf0a 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/SmartCompositeMessageConverter.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/SmartCompositeMessageConverter.java @@ -16,9 +16,16 @@ package org.springframework.cloud.function.context.config; +import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collection; +import java.util.Iterator; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -26,6 +33,7 @@ import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.SmartMessageConverter; +import org.springframework.messaging.support.MessageBuilder; import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.util.MimeType; import org.springframework.util.StringUtils; @@ -33,10 +41,13 @@ /** * * @author Oleg Zhurakousky + * @author Salvatore Bernardo * */ public class SmartCompositeMessageConverter extends CompositeMessageConverter { + private Log logger = LogFactory.getLog(this.getClass()); + public SmartCompositeMessageConverter(Collection converters) { super(converters); } @@ -45,26 +56,72 @@ public SmartCompositeMessageConverter(Collection converters) { @Nullable public Object fromMessage(Message message, Class targetClass) { for (MessageConverter converter : getConverters()) { - Object result = converter.fromMessage(message, targetClass); - if (result != null) { - return result; + if (!(message.getPayload() instanceof byte[]) && targetClass.isInstance(message.getPayload()) && !(message.getPayload() instanceof Collection)) { + return message.getPayload(); + } + try { + Object result = converter.fromMessage(message, targetClass); + if (result != null) { + return result; + } + } + catch (Exception e) { + if (logger.isWarnEnabled()) { + logger.warn("Failure during type conversion by " + converter + ". Will try the next converter.", e); + } } } return null; } + @SuppressWarnings("unchecked") @Override - @Nullable public Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint) { - for (MessageConverter converter : getConverters()) { - Object result = (converter instanceof SmartMessageConverter ? - ((SmartMessageConverter) converter).fromMessage(message, targetClass, conversionHint) : - converter.fromMessage(message, targetClass)); - if (result != null) { - return result; + if (!(message.getPayload() instanceof byte[]) && targetClass.isInstance(message.getPayload()) && !(message.getPayload() instanceof Collection)) { + return message.getPayload(); + } + Object result = null; + if (message.getPayload() instanceof Iterable && conversionHint != null) { + Iterable iterablePayload = (Iterable) message.getPayload(); + Type genericItemType = FunctionTypeUtils.getImmediateGenericType((Type) conversionHint, 0); + Class genericItemRawType = FunctionTypeUtils.getRawType(genericItemType); + List resultList = new ArrayList<>(); + for (Object item : iterablePayload) { + boolean isConverted = false; + if (item.getClass().getName().startsWith("org.springframework.kafka.support.KafkaNull")) { + resultList.add(null); + isConverted = true; + } + for (Iterator iterator = getConverters().iterator(); iterator.hasNext() && !isConverted;) { + MessageConverter converter = (MessageConverter) iterator.next(); + if (!converter.getClass().getName().endsWith("ApplicationJsonMessageMarshallingConverter")) { // TODO Stream stuff, needs to be removed + Message m = MessageBuilder.withPayload(item).copyHeaders(message.getHeaders()).build(); // TODO Message creating may be expensive + Object conversionResult = (converter instanceof SmartMessageConverter & genericItemRawType != genericItemType ? + ((SmartMessageConverter) converter).fromMessage(m, genericItemRawType, genericItemType) : + converter.fromMessage(m, genericItemRawType)); + if (conversionResult != null) { + resultList.add(conversionResult); + isConverted = true; + } + } + } } + result = resultList; } - return null; + else { + for (MessageConverter converter : getConverters()) { + if (!converter.getClass().getName().endsWith("ApplicationJsonMessageMarshallingConverter")) { // TODO Stream stuff, needs to be removed + result = (converter instanceof SmartMessageConverter ? + ((SmartMessageConverter) converter).fromMessage(message, targetClass, conversionHint) : + converter.fromMessage(message, targetClass)); + if (result != null) { + return result; + } + } + } + } + + return result; } @Override @@ -79,15 +136,17 @@ public Message toMessage(Object payload, @Nullable MessageHeaders headers) { String[] contentTypes = StringUtils.delimitedListToStringArray((String) value, ","); for (String contentType : contentTypes) { if (!MimeType.valueOf(contentType).isConcrete()) { - List supportedMimeTypes = ((AbstractMessageConverter) converter).getSupportedMimeTypes(); - for (MimeType supportedMimeType : supportedMimeTypes) { - if (supportedMimeType.isCompatibleWith(MimeType.valueOf(contentType))) { - MessageHeaderAccessor h = new MessageHeaderAccessor(); - h.copyHeaders(headers); - h.setHeader(MessageHeaders.CONTENT_TYPE, supportedMimeType); - Message result = converter.toMessage(payload, h.getMessageHeaders()); - if (result != null) { - return result; + if (converter instanceof AbstractMessageConverter) { + List supportedMimeTypes = ((AbstractMessageConverter) converter).getSupportedMimeTypes(); + for (MimeType supportedMimeType : supportedMimeTypes) { + if (supportedMimeType.isCompatibleWith(MimeType.valueOf(contentType))) { + MessageHeaderAccessor h = new MessageHeaderAccessor(); + h.copyHeaders(headers); + h.setHeader(MessageHeaders.CONTENT_TYPE, supportedMimeType); + Message result = converter.toMessage(payload, h.getMessageHeaders()); + if (result != null) { + return result; + } } } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java index 9152df071..eed921215 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/converter/avro/AvroSchemaServiceManager.java @@ -28,8 +28,7 @@ * Helps to substitute the default implementation of {@link org.apache.avro.Schema} * Generation using Custom Avro schema generator * - * Provide a custom bean definition of {@link AvroSchemaServiceManager} and mark - * it as @Primary to override the default implementation + * Provide a custom bean definition of {@link AvroSchemaServiceManager} to override the default implementation. * * Migrating this interface from the original Spring Cloud Schema Registry project. * diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/message/MessageUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/message/MessageUtils.java index 2c6b3c7bd..b6652e4ee 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/message/MessageUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/message/MessageUtils.java @@ -20,6 +20,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.TreeMap; import org.springframework.cloud.function.core.FluxWrapper; import org.springframework.cloud.function.core.Isolated; @@ -28,6 +29,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; + /** * @author Dave Syer * @author Oleg Zhurakousky @@ -50,6 +52,7 @@ public abstract class MessageUtils { public static String SOURCE_TYPE = "source-type"; /** +<<<<<<< HEAD * Create a message for the handler. If the handler is a wrapper for a function in an * isolated class loader, then the message will be created with the target class * loader (therefore the {@link Message} class must be on the classpath of the target @@ -124,4 +127,34 @@ public static Message unpack(Object handler, Object message) { return MessageBuilder.withPayload(payload).copyHeaders(headers).build(); } + /** + * Returns (payload, headers) structure identical to `message` while + * substituting headers with case insensitive map. + */ + public static MessageStructureWithCaseInsensitiveHeaderKeys toCaseInsensitiveHeadersStructure(Message message) { + return new MessageStructureWithCaseInsensitiveHeaderKeys(message); + } + + /** + * !!! INTERNAL USE ONLY, MAY CHANGE OR REMOVED WITHOUT NOTICE!!! + */ + @SuppressWarnings({"rawtypes"}) + public static class MessageStructureWithCaseInsensitiveHeaderKeys { + private final Object payload; + private final Map headers; + + @SuppressWarnings("unchecked") + MessageStructureWithCaseInsensitiveHeaderKeys(Message message) { + this.payload = message.getPayload(); + this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + this.headers.putAll(message.getHeaders()); + } + public Object getPayload() { + return payload; + } + + public Map getHeaders() { + return headers; + } + } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventFunctionTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventFunctionTests.java index 388b0f057..0ad602b4e 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventFunctionTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventFunctionTests.java @@ -391,6 +391,7 @@ Function, Mono> springReleaseReacti Function, Message> springReleaseAsMessage() { return message -> { SpringReleaseEvent updated = springRelease().apply(message.getPayload()); + assertThat(message.getHeaders().get("ce-type")).isEqualTo("org.springframework"); return CloudEventMessageBuilder.withData(updated) .copyHeaders(message.getHeaders()) .setSource("https://spring.release.event") diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtilsAndBuilderTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtilsAndBuilderTests.java index 4745e2f2a..3bf9e313b 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtilsAndBuilderTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/cloudevent/CloudEventMessageUtilsAndBuilderTests.java @@ -31,6 +31,19 @@ */ public class CloudEventMessageUtilsAndBuilderTests { + @Test// see https://github.com/spring-cloud/spring-cloud-function/issues/805 + public void testHeaderKeyInsensitivity() { + Message httpMessage = MessageBuilder.withPayload("hello") + .setHeader("cE-SoUrCe", "https://foo.bar") + .setHeader("Ce-specVeRsion", "1.0") + .setHeader("Ce-Type", "blah") + .setHeader("x", "x") + .setHeader("zzz", "zzz") + .build(); + + assertThat(CloudEventMessageUtils.isCloudEvent(httpMessage)).isTrue(); + } + @Test// see https://github.com/spring-cloud/spring-cloud-function/issues/680 public void testProperAttributeExtractionRegardlessOfTargetProtocol() { Message ceMessage = CloudEventMessageBuilder.withData("foo").build(); diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/FunctionRegistrationTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/FunctionRegistrationTests.java index 10f755f55..1e2e31387 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/FunctionRegistrationTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/FunctionRegistrationTests.java @@ -36,16 +36,6 @@ public void noTypeByDefault() { assertThat(registration.getNames()).contains("foos"); } - @Test - public void wrap() { - FunctionRegistration registration = new FunctionRegistration<>(new Foos(), - "foos").type(FunctionType.of(Foos.class).getType()); - FunctionRegistration other = registration.wrap(); - assertThat(registration.getType().isWrapper()).isFalse(); - assertThat(other.getType().isWrapper()).isTrue(); - assertThat(other.getTarget()).isNotEqualTo(registration.getTarget()); - } - private static class Foos implements Function { @Override diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/HybridFunctionalRegistrationTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/HybridFunctionalRegistrationTests.java index f49d301be..fd8b3d843 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/HybridFunctionalRegistrationTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/HybridFunctionalRegistrationTests.java @@ -26,6 +26,8 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +39,7 @@ public class HybridFunctionalRegistrationTests { // see https://github.com/spring-cloud/spring-cloud-function/issues/258 + @SuppressWarnings("rawtypes") @Test public void testNoDoubleRegistrationInHybridMode() { ConfigurableApplicationContext context = FunctionalSpringApplication @@ -46,9 +49,26 @@ public void testNoDoubleRegistrationInHybridMode() { assertThat(context.containsBean("function")).isTrue(); assertThat(context.getBeansOfType(UppercaseFunction.class).size()).isEqualTo(1); - assertThat((Object) catalog.lookup(Function.class, "hybridFunctionalRegistrationTests.UppercaseFunction")).isNotNull(); + assertThat((Function) catalog.lookup("hybridFunctionalRegistrationTests.UppercaseFunction")).isNotNull(); } + @SuppressWarnings("rawtypes") + @Test + public void testMessageHeaderPropagationInFunctionalBeanRegistration() { + ConfigurableApplicationContext context = FunctionalSpringApplication + .run(UppercaseMessageFunction.class, "--spring.functional.enabled=false"); + + FunctionCatalog catalog = context.getBean(FunctionCatalog.class); + + assertThat(context.containsBean("function")).isTrue(); + assertThat(context.getBeansOfType(UppercaseMessageFunction.class).size()).isEqualTo(1); + Function f = catalog.lookup(Function.class, "hybridFunctionalRegistrationTests.UppercaseMessageFunction"); + assertThat(f).isNotNull(); + String result = (String) f.apply(MessageBuilder.withPayload("hello").setHeader("foo", "foo").setHeader("blah", "blah").build()); + assertThat(result).isEqualTo("HELLO"); + } + + @SuppressWarnings("rawtypes") @Test public void testNoDoubleRegistrationInHybridModeFluxedFunction() { ConfigurableApplicationContext context = FunctionalSpringApplication @@ -58,7 +78,7 @@ public void testNoDoubleRegistrationInHybridModeFluxedFunction() { assertThat(context.containsBean("function")).isTrue(); assertThat(context.getBeansOfType(UppercaseFluxFunction.class).size()).isEqualTo(1); - assertThat((Object) catalog.lookup(Function.class, "hybridFunctionalRegistrationTests.UppercaseFluxFunction")).isNotNull(); + assertThat((Function) catalog.lookup(Function.class, "hybridFunctionalRegistrationTests.UppercaseFluxFunction")).isNotNull(); } @SpringBootConfiguration @@ -74,6 +94,21 @@ public String apply(String t) { } } + @SpringBootConfiguration + @ImportAutoConfiguration({ + ContextFunctionCatalogAutoConfiguration.class, + JacksonAutoConfiguration.class } + ) + public static class UppercaseMessageFunction implements Function, String> { + + @Override + public String apply(Message message) { + assertThat(message.getHeaders().get("foo")).isEqualTo("foo"); + assertThat(message.getHeaders().get("blah")).isEqualTo("blah"); + return message.getPayload().toUpperCase(); + } + } + @SpringBootConfiguration @ImportAutoConfiguration({ ContextFunctionCatalogAutoConfiguration.class, diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java index 47318beb8..f8511d525 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryTests.java @@ -22,8 +22,10 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -32,11 +34,13 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import com.fasterxml.jackson.databind.JsonNode; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -57,6 +61,7 @@ import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.lang.Nullable; @@ -101,6 +106,68 @@ public void before() { System.clearProperty("spring.cloud.function.definition"); } + @Test + public void testFunctionEligibilityFiltering() { + System.setProperty("spring.cloud.function.ineligible-definitions", "asJsonNode"); + Collection registeredFunction = new ArrayList(); + FunctionCatalog catalog = this.configureCatalog(JsonNodeConfiguration.class); + for (String beanName : context.getBeanDefinitionNames()) { + try { + FunctionInvocationWrapper function = catalog.lookup(beanName); + if (function != null && function.getFunctionDefinition().equals(beanName)) { + registeredFunction.add(function); + } + } + catch (Exception e) { + // ignore + } + } + System.out.println(registeredFunction); + assertThat(registeredFunction.size()).isEqualTo(2); + assertThat((FunctionInvocationWrapper) catalog.lookup("asJsonNode")).isNull(); + } + + @Test + public void testJsonNodeAsInput() throws Exception { + FunctionCatalog catalog = this.configureCatalog(JsonNodeConfiguration.class); + Function, Message> f = catalog.lookup("messageAsJsonNode", "application/json"); + Message m = MessageBuilder.withPayload("[{\"name\":\"bob\"}, {\"name\":\"bob\"}]").setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build(); + assertThat(new String(f.apply(m).getPayload())).isEqualTo("[{\"name\":\"bob\"},{\"name\":\"bob\"}]"); + f = catalog.lookup("asJsonNode", "application/json"); + assertThat(new String(f.apply(m).getPayload())).isEqualTo("[{\"name\":\"bob\"},{\"name\":\"bob\"}]"); + } + + @SuppressWarnings({ "rawtypes" }) + @Test + public void concurrencyLookupTest() throws Exception { + FunctionCatalog catalog = this.configureCatalog(); + ExecutorService executor = Executors.newCachedThreadPool(); + for (int i = 0; i < 100; i++) { + executor.execute(() -> { + catalog.lookup("uppercase", "application/json"); + }); + executor.execute(() -> { + catalog.lookup("numberword", "application/json"); + }); + } + Thread.sleep(1000); + Field frField = ReflectionUtils.findField(catalog.getClass(), "functionRegistrations"); + frField.setAccessible(true); + Collection c = (Collection) frField.get(catalog); + assertThat(c.size()).isEqualTo(2); + } + + @Test + public void testReturnedMessageIsUnmodified() throws Exception { + FunctionCatalog catalog = this.configureCatalog(); + Function, Message> function = catalog.lookup("uppercaseMessage", "application/json"); + assertThat(function).isNotNull(); + + Message result = function.apply(MessageBuilder.withPayload("bob").setHeader("foo", "foo").build()); + assertThat(result.getHeaders().containsKey("foo")).isFalse(); + assertThat(result.getHeaders().containsKey("bar")).isTrue(); + } + @SuppressWarnings("unchecked") @Test public void testDefaultLookup() throws Exception { @@ -130,17 +197,18 @@ public void testDefaultLookup() throws Exception { assertThat(((FunctionInvocationWrapper) function).isComposed()).isTrue(); } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Test - public void testImperativeFunction() { + public void testBiFunction() { FunctionCatalog catalog = this.configureCatalog(); -// Function asIs = catalog.lookup("uppercase"); -// assertThat(asIs.apply("uppercase")).isEqualTo("UPPERCASE"); -// -// Function, Flux> asFlux = catalog.lookup("uppercase"); -// List result = asFlux.apply(Flux.just("uppercaseFlux", "uppercaseFlux2")).collectList().block(); -// assertThat(result.get(0)).isEqualTo("UPPERCASEFLUX"); -// assertThat(result.get(1)).isEqualTo("UPPERCASEFLUX2"); + Function biFunction = catalog.lookup("biFuncUpperCase"); + assertThat(biFunction.apply("hello")).isEqualTo("HELLO"); + } + + @Test + public void testImperativeFunction() { + FunctionCatalog catalog = this.configureCatalog(); Function>, Flux>> messageFlux = catalog.lookup("uppercase", "application/json"); Message message1 = MessageBuilder.withPayload("\"uppercaseFlux\"".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build(); @@ -417,18 +485,25 @@ public void byteArrayNoSpecialHandling() throws Exception { assertThat(result.getPayload()).isEqualTo("\"b2xsZWg=\"".getBytes()); } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void testMultipleValuesInOutputHandling() throws Exception { FunctionCatalog catalog = this.configureCatalog(CollectionOutConfiguration.class); FunctionInvocationWrapper function = catalog.lookup("parseToList", "application/json"); assertThat(function).isNotNull(); - Object result = function.apply(MessageBuilder.withPayload("1, 2, 3".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build()); + Object result = function.apply(MessageBuilder.withPayload("1,2,3".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build()); assertThat(result instanceof Message).isTrue(); + byte[] payload = ((Message) result).getPayload(); + JsonMapper mapper = this.context.getBean(JsonMapper.class); + List resultList = mapper.fromJson(payload, List.class); + assertThat(resultList.size()).isEqualTo(3); + assertThat(resultList.get(0)).isEqualTo("1"); + assertThat(resultList.get(1)).isEqualTo("2"); function = catalog.lookup("parseToListOfMessages", "application/json"); assertThat(function).isNotNull(); - result = function.apply(MessageBuilder.withPayload("1, 2, 3".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build()); - assertThat(result instanceof Message).isFalse(); + result = function.apply(MessageBuilder.withPayload("1,2,3".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build()); + assertThat(result instanceof List).isTrue(); } /** @@ -526,6 +601,18 @@ public void testRegisteringWithTypeThatDoesNotMatchDiscoveredType() { registry.register(e); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testNoConversionOnInputMapIfInputIsMap() { + FunctionCatalog catalog = this.configureCatalog(); + Function f = catalog.lookup("maptopojo"); + Person p = new Person("John", 123); + Map map = new HashMap<>(); + map.put("person", p); + map.put("foo", "foo"); + assertThat(f.apply(map)).isInstanceOf(Person.class); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void testValueWrappedInMessageIfNecessary() { @@ -653,6 +740,29 @@ public void testGH_768() throws Exception { assertThat(result).startsWith("{date="); } + @Test + public void test_791() { + try (ConfigurableApplicationContext ac = new SpringApplicationBuilder(InputHeaderPropagationConfiguration.class) + .run("--logging.level.org.springframework.cloud.function=DEBUG", + "--spring.main.lazy-initialization=true")) { + FunctionCatalog catalog = ac.getBean(FunctionCatalog.class); + + Function, Message> uppercase = catalog.lookup("uppercase", "application/json"); + Message result = uppercase.apply(MessageBuilder.withPayload("bob").setHeader("foo", "bar").build()); + assertThat(result.getHeaders()).doesNotContainKey("foo"); + } + try (ConfigurableApplicationContext ac = new SpringApplicationBuilder(InputHeaderPropagationConfiguration.class) + .run("--logging.level.org.springframework.cloud.function=DEBUG", + "--spring.main.lazy-initialization=true", + "--spring.cloud.function.configuration.uppercase.copy-input-headers=true")) { + FunctionCatalog catalog = ac.getBean(FunctionCatalog.class); + + Function, Message> uppercase = catalog.lookup("uppercase", "application/json"); + Message result = uppercase.apply(MessageBuilder.withPayload("bob").setHeader("foo", "bar").build()); + assertThat(result.getHeaders()).containsKey("foo"); + } + } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void testArrayPayloadOnFluxFunction() throws Exception { @@ -716,6 +826,24 @@ public Function, String> uppercasePerson() { } } + @EnableAutoConfiguration + @Configuration + public static class JsonNodeConfiguration { + @Bean + public Function, String> messageAsJsonNode() { + return v -> { + return v.getPayload().toString(); + }; + } + + @Bean + public Function asJsonNode() { + return v -> { + return v.toString(); + }; + } + } + @EnableAutoConfiguration public static class EmptyConfiguration { @@ -904,6 +1032,16 @@ protected Object doApply(Object input, FunctionInvocationWrapper targetFunction) } } + @EnableAutoConfiguration + @Configuration + protected static class InputHeaderPropagationConfiguration { + + @Bean + public Function uppercase() { + return x -> x.toUpperCase(); + } + } + @EnableAutoConfiguration @Configuration protected static class SampleFunctionConfiguration { @@ -927,10 +1065,17 @@ public Supplier numberword() { return () -> "one"; } + @Bean + public BiFunction biFuncUpperCase() { + return (p, h) -> { + return p.toUpperCase(); + }; + } + @Bean public Function, Person> maptopojo() { return map -> { - Person person = new Person((String) map.get("name"), Integer.parseInt((String) map.get("id"))); + Person person = (Person) map.get("person"); return person; }; } @@ -940,6 +1085,15 @@ public Function uppercase() { return v -> v.toUpperCase(); } + @Bean + public Function, Message> uppercaseMessage() { + return message -> { + Message result = MessageBuilder.fromMessage(message) + .removeHeader("foo").setHeader("bar", "bar").build(); + return result; + }; + } + @Bean public Function consumerFunction() { return v -> { diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtilsTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtilsTests.java index 6f0285422..e295b830e 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtilsTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/FunctionTypeUtilsTests.java @@ -17,10 +17,13 @@ package org.springframework.cloud.function.context.catalog; +import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.Date; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -32,8 +35,10 @@ import reactor.util.function.Tuple3; import org.springframework.cloud.function.context.FunctionType; +import org.springframework.core.MethodParameter; import org.springframework.core.ParameterizedTypeReference; import org.springframework.messaging.Message; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -43,7 +48,7 @@ * */ @SuppressWarnings("unused") -public class FunctionTypeUtilsTests { +public class FunctionTypeUtilsTests { @Test public void testFunctionTypeFrom() throws Exception { @@ -148,6 +153,21 @@ public void testIsTypeCollection() { assertThat(FunctionTypeUtils.isTypeCollection(new ParameterizedTypeReference>>>() { }.getType())).isFalse(); } + @Test + public void testNoNpeFromIsMessage() { + FunctionTypeUtilsTests testService = new FunctionTypeUtilsTests<>(); + + Method methodUnderTest = + ReflectionUtils.findMethod(testService.getClass(), "notAMessageMethod", AtomicReference.class); + MethodParameter methodParameter = MethodParameter.forExecutable(methodUnderTest, 0); + + assertThat(FunctionTypeUtils.isMessage(methodParameter.getGenericParameterType())).isFalse(); + } + + void notAMessageMethod(AtomicReference payload) { + + } + private static Function function() { return null; } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistryTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistryTests.java index 062b2d7fa..2e2f9fb0d 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistryTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/SimpleFunctionRegistryTests.java @@ -16,17 +16,23 @@ package org.springframework.cloud.function.context.catalog; +import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import java.util.stream.IntStream; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; @@ -47,6 +53,7 @@ import org.springframework.cloud.function.context.HybridFunctionalRegistrationTests.UppercaseFunction; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.context.config.JsonMessageConverter; +import org.springframework.cloud.function.context.config.SmartCompositeMessageConverter; import org.springframework.cloud.function.json.GsonMapper; import org.springframework.cloud.function.json.JacksonMapper; import org.springframework.cloud.function.json.JsonMapper; @@ -65,6 +72,7 @@ import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.MimeType; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -85,11 +93,32 @@ public void before() { messageConverters.add(new JsonMessageConverter(jsonMapper)); messageConverters.add(new ByteArrayMessageConverter()); messageConverters.add(new StringMessageConverter()); - this.messageConverter = new CompositeMessageConverter(messageConverters); + this.messageConverter = new SmartCompositeMessageConverter(messageConverters); this.conversionService = new DefaultConversionService(); } + @SuppressWarnings("rawtypes") + @Test + public void concurrencyRegistrationTest() throws Exception { + Echo function = new Echo(); + FunctionRegistration registration = new FunctionRegistration<>( + function, "echo").type(FunctionType.of(Echo.class)); + SimpleFunctionRegistry catalog = new SimpleFunctionRegistry(this.conversionService, this.messageConverter, + new JacksonMapper(new ObjectMapper())); + ExecutorService executor = Executors.newCachedThreadPool(); + for (int i = 0; i < 1000; i++) { + executor.execute(() -> { + catalog.register(registration); + }); + } + Thread.sleep(1000); + Field frField = ReflectionUtils.findField(catalog.getClass(), "functionRegistrations"); + frField.setAccessible(true); + Collection c = (Collection) frField.get(catalog); + assertThat(c.size()).isEqualTo(1); + } + @Test public void testCachingOfFunction() { Echo function = new Echo(); @@ -527,6 +556,30 @@ public void testReactiveMonoSupplier() { assertThat(FunctionTypeUtils.isMono(function.getOutputType())); } + @Test + public void testFunctionCompositionWithReactiveSupplierAndConsumer() { + SimpleFunctionRegistry catalog = new SimpleFunctionRegistry(this.conversionService, this.messageConverter, + new JacksonMapper(new ObjectMapper())); + + Object reactiveFunc = reactiveFluxSupplier(); + FunctionRegistration functionRegistration = new FunctionRegistration(reactiveFunc, "reactiveFluxSupplier") + .type(ResolvableType.forClassWithGenerics( + Supplier.class, ResolvableType.forClassWithGenerics(Flux.class, String.class)).getType()); + catalog.register(functionRegistration); + + reactiveFunc = reactiveFluxConsumer(); + functionRegistration = new FunctionRegistration(reactiveFunc, "reactiveFluxConsumer") + .type(ResolvableType.forClassWithGenerics( + Consumer.class, ResolvableType.forClassWithGenerics(Flux.class, String.class)).getType()); + catalog.register(functionRegistration); + + FunctionInvocationWrapper lookedUpFunction = catalog + .lookup("reactiveFluxSupplier|reactiveFluxConsumer"); + + assertThat(lookedUpFunction).isNotNull(); + lookedUpFunction.apply(null); + assertThat(consumerDowncounter.get()).isZero(); + } public Function uppercase() { return v -> v.toUpperCase(); @@ -551,6 +604,18 @@ public Consumer> reactiveConsumer() { }); } + private final AtomicInteger consumerDowncounter = new AtomicInteger(10); + + public Supplier> reactiveFluxSupplier() { + return () -> Flux.fromStream( + IntStream.range(0, consumerDowncounter.get()).boxed().map(i -> Integer.toString(i)) + ); + } + + public Consumer> reactiveFluxConsumer() { + return flux -> flux.subscribe(v -> consumerDowncounter.decrementAndGet()); + } + private FunctionCatalog configureCatalog(Class... configClass) { ApplicationContext context = new SpringApplicationBuilder(configClass) .run("--logging.level.org.springframework.cloud.function=DEBUG", diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationConditionalLoadingTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationConditionalLoadingTests.java new file mode 100644 index 000000000..ebfd022b1 --- /dev/null +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationConditionalLoadingTests.java @@ -0,0 +1,144 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.context.config; + +import io.cloudevents.spring.messaging.CloudEventMessageConverter; +import org.apache.avro.Schema; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionRegistry; +import org.springframework.cloud.function.context.converter.avro.AvroSchemaMessageConverter; +import org.springframework.cloud.function.context.converter.avro.AvroSchemaServiceManager; +import org.springframework.cloud.function.context.scan.TestFunction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests the conditional loading aspects of the {@link ContextFunctionCatalogAutoConfiguration}. + * + * @author Chris Bono + * @author Soby Chacko + */ +public class ContextFunctionCatalogAutoConfigurationConditionalLoadingTests { + + protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(ContextFunctionCatalogAutoConfiguration.class)); + + @Test + void autoConfigDisabledWhenCustomFunctionCatalogExists() { + contextRunner.withBean(FunctionCatalog.class, () -> mock(FunctionCatalog.class)) + .run((context) -> assertThat(context).doesNotHaveBean(FunctionRegistry.class)); + } + + @Nested + class AvroSchemaMessageConverterConfig { + + @Test + void avroSchemaMessageConverterBeansLoadedWhenAvroOnClasspath() { + contextRunner.run((context) -> assertThat(context).hasSingleBean(AvroSchemaServiceManager.class) + .hasSingleBean(AvroSchemaMessageConverter.class)); + } + + @Test + void avroSchemaMessageConverterBeansNotLoadedWhenAvroOnClasspathButDisabledThroughProperty() { + contextRunner.withPropertyValues("spring.cloud.stream.avro.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(AvroSchemaServiceManager.class) + .doesNotHaveBean(AvroSchemaMessageConverter.class)); + } + + @Test + void avroSchemaMessageConverterBeansNotLoadedWhenAvroNotOnClasspath() { + contextRunner.withClassLoader(new FilteredClassLoader(Schema.class)).run((context) -> + assertThat(context).doesNotHaveBean(AvroSchemaServiceManager.class) + .doesNotHaveBean(AvroSchemaMessageConverter.class)); + } + + @Test + void customAvroSchemaServiceManagerIsRespected() { + AvroSchemaServiceManager customManager = mock(AvroSchemaServiceManager.class); + contextRunner.withBean(AvroSchemaServiceManager.class, () -> customManager) + .run((context) -> assertThat(context).getBean(AvroSchemaServiceManager.class).isSameAs(customManager)); + } + + @Test + void customAvroSchemaMessageConverterIsRespected() { + AvroSchemaMessageConverter customConverter = mock(AvroSchemaMessageConverter.class); + contextRunner.withBean(AvroSchemaMessageConverter.class, () -> customConverter) + .run((context) -> assertThat(context).getBean(AvroSchemaMessageConverter.class).isSameAs(customConverter)); + } + } + + @Nested + class CloudEventsMessageConverterConfig { + + @Test + void cloudEventsMessageConverterBeanLoadedWhenCloudEventsOnClasspath() { + contextRunner.run((context) -> assertThat(context).hasSingleBean(CloudEventMessageConverter.class)); + } + + @Test + void cloudEventsMessageConverterBeanNotLoadedWhenCloudEventsNotOnClasspath() { + contextRunner.withClassLoader(new FilteredClassLoader(CloudEventMessageConverter.class)).run((context) -> + assertThat(context).doesNotHaveBean(CloudEventMessageConverter.class)); + } + + @Test + void customCloudEventsMessageConverterIsRespected() { + CloudEventMessageConverter customConverter = mock(CloudEventMessageConverter.class); + contextRunner.withBean(CloudEventMessageConverter.class, () -> customConverter) + .run((context) -> assertThat(context).getBean(CloudEventMessageConverter.class).isSameAs(customConverter)); + } + } + + @Nested + class PlainFunctionScanConfig { + + @Test + void functionScanConfigEnabledByDefault() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackage().getName()) + .run((context) -> assertThat(context).hasSingleBean(TestFunction.class)); + } + + @Test + void functionScanConfigEnabledWhenEnabledPropertySetToTrue() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackage().getName(), + "spring.cloud.function.scan.enabled:true") + .run((context) -> assertThat(context).hasSingleBean(TestFunction.class)); + } + + @Test + void functionScanConfigEnabledWithScanPackagesPointingToNoFunctions() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackage().getName() + ".faux", + "spring.cloud.function.scan.enabled:true") + .run((context) -> assertThat(context).doesNotHaveBean(TestFunction.class)); + } + + @Test + void functionScanConfigDisabledWhenEnabledPropertySetToFalse() { + contextRunner.withPropertyValues("spring.cloud.function.scan.packages:" + TestFunction.class.getPackage().getName(), + "spring.cloud.function.scan.enabled:false") + .run((context) -> assertThat(context).doesNotHaveBean(TestFunction.class)); + } + + } +} diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java index cb9267b91..0f87cabca 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java @@ -16,6 +16,8 @@ package org.springframework.cloud.function.context.config; +import java.util.HashMap; +import java.util.Map; import java.util.function.Function; import org.junit.jupiter.api.AfterEach; @@ -24,17 +26,23 @@ import reactor.core.publisher.Flux; import reactor.test.StepVerifier; +import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.DefaultMessageRoutingHandler; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionProperties; +import org.springframework.cloud.function.context.MessageRoutingCallback; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; /** * @@ -52,13 +60,42 @@ public void before() { context.close(); } - private FunctionCatalog configureCatalog() { - context = new SpringApplicationBuilder(RoutingFunctionConfiguration.class).run( + private FunctionCatalog configureCatalog(Class configurationClass) { + context = new SpringApplicationBuilder(configurationClass).run( "--logging.level.org.springframework.cloud.function=DEBUG", "--spring.cloud.function.routing.enabled=true"); return context.getBean(FunctionCatalog.class); } + private FunctionCatalog configureCatalog() { + return configureCatalog(RoutingFunctionConfiguration.class); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testDefaultRouting() { + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".definition", "blah").build(); + + FunctionCatalog functionCatalog = this.configureCatalog(EmptyConfiguration.class); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + try { + function.apply(message); + fail("Should not be here"); + } + catch (Exception e) { + // Good + } + // + functionCatalog = this.configureCatalog(ConfigurationWithDefaultMessageRoutingHandler.class); + function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + function.apply(message); + ConfigurationWithDefaultMessageRoutingHandler config = this.context.getBean(ConfigurationWithDefaultMessageRoutingHandler.class); + assertThat(config.defaultHandlerInvoked).isTrue(); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void testInvocationWithMessageAndHeader() { @@ -91,10 +128,7 @@ public void testRoutingReactiveInputWithReactiveFunctionAndDefinitionMessageHead .setHeader(FunctionProperties.PREFIX + ".definition", "echoFlux").build(); Flux resultFlux = (Flux) function.apply(Flux.just(message)); - StepVerifier - .create(resultFlux) - .expectError() - .verify(); + StepVerifier.create(resultFlux).expectError().verify(); } @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -106,10 +140,27 @@ public void testRoutingReactiveInputWithReactiveFunctionAndExpressionMessageHead Message message = MessageBuilder.withPayload("hello") .setHeader(FunctionProperties.PREFIX + ".routing-expression", "'echoFlux'").build(); Flux resultFlux = (Flux) function.apply(Flux.just(message)); - StepVerifier - .create(resultFlux) - .expectError() - .verify(); + StepVerifier.create(resultFlux).expectError().verify(); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void failWithHeaderProvidedExpressionAccessingRuntime() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".routing-expression", + "T(java.lang.Runtime).getRuntime().exec(\"open -a calculator.app\")") + .build(); + try { + function.apply(message); + fail("Function shoudl not succeed"); + } + catch (Exception e) { + assertThat(e.getMessage()).isEqualTo("EL1005E: Type cannot be found 'java.lang.Runtime'"); + } + } @SuppressWarnings({ "unchecked", "rawtypes" }) @@ -134,10 +185,25 @@ public void testInvocationWithMessageAndRoutingExpression() { assertThat(function.apply(message)).isEqualTo("olleh"); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testInvocationWithMessageAndRoutingExpressionCaseInsensitive() { + System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "headers.function_Name"); + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello").setHeader("function_name", "reverse").build(); + assertThat(function.apply(message)).isEqualTo("olleh"); + + System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "headers.FunCtion_namE"); + assertThat(function.apply(message)).isEqualTo("olleh"); + } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void testInvocationWithRoutingBeanExpression() { - System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "@reverse.apply(#root.getHeaders().get('func'))"); + System.setProperty(FunctionProperties.PREFIX + ".routing-expression", + "@reverse.apply(#root.getHeaders().get('func'))"); FunctionCatalog functionCatalog = this.configureCatalog(); Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); assertThat(function).isNotNull(); @@ -156,16 +222,17 @@ public void testOtherExpectedFailures() { Assertions.fail(); } catch (Exception e) { - //ignore + // ignore } // non existing function try { - function.apply(MessageBuilder.withPayload("hello").setHeader(FunctionProperties.PREFIX + ".definition", "blah").build()); + function.apply(MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".definition", "blah").build()); Assertions.fail(); } catch (Exception e) { - //ignore + // ignore } } @@ -183,6 +250,22 @@ public void testInvocationWithMessageComposed() { assertThat(function.apply(message)).isEqualTo("OLLEH"); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testMultipleRouters() { + System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "'uppercase'"); + FunctionCatalog functionCatalog = this.configureCatalog(MultipleRouterConfiguration.class); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello").build(); + assertThat(function.apply(message)).isEqualTo("HELLO"); + + function = functionCatalog.lookup("mySpecialRouter"); + assertThat(function).isNotNull(); + message = MessageBuilder.withPayload("hello").build(); + assertThat(function.apply(message)).isEqualTo("olleh"); + } + @EnableAutoConfiguration @Configuration protected static class RoutingFunctionConfiguration { @@ -202,4 +285,47 @@ public Function, Flux> echoFlux() { return f -> f; } } + + @EnableAutoConfiguration + @Configuration + protected static class MultipleRouterConfiguration { + + @Bean + RoutingFunction mySpecialRouter(FunctionCatalog functionCatalog, BeanFactory beanFactory, @Nullable MessageRoutingCallback routingCallback) { + Map propertiesMap = new HashMap<>(); + propertiesMap.put(FunctionProperties.PREFIX + ".routing-expression", "'reverse'"); + return new RoutingFunction(functionCatalog, propertiesMap, new BeanFactoryResolver(beanFactory), routingCallback); + } + + @Bean + public Function reverse() { + return v -> new StringBuilder(v).reverse().toString(); + } + + @Bean + public Function uppercase() { + return String::toUpperCase; + } + } + + @EnableAutoConfiguration + @Configuration + protected static class EmptyConfiguration { + } + + @EnableAutoConfiguration + @Configuration + protected static class ConfigurationWithDefaultMessageRoutingHandler { + public boolean defaultHandlerInvoked; + @Bean + public DefaultMessageRoutingHandler defaultRoutingHandler() { + return new DefaultMessageRoutingHandler() { + @Override + public void accept(Message message) { + super.accept(message); + defaultHandlerInvoked = true; + } + }; + } + } } diff --git a/spring-cloud-function-core/pom.xml b/spring-cloud-function-core/pom.xml index fb4a3e1c4..5f1a674d0 100644 --- a/spring-cloud-function-core/pom.xml +++ b/spring-cloud-function-core/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-dependencies/pom.xml b/spring-cloud-function-dependencies/pom.xml index 04275c8e8..b2d04a903 100644 --- a/spring-cloud-function-dependencies/pom.xml +++ b/spring-cloud-function-dependencies/pom.xml @@ -6,11 +6,11 @@ spring-cloud-dependencies-parent org.springframework.cloud - 3.1.1-SNAPSHOT + 3.1.10-SNAPSHOT spring-cloud-function-dependencies - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT pom Spring Cloud Function Dependencies Spring Cloud Function Dependencies diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml index 1bbf34116..f02bb89f1 100644 --- a/spring-cloud-function-deployer/pom.xml +++ b/spring-cloud-function-deployer/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-deployer/src/it/bootapp-multi/pom.xml b/spring-cloud-function-deployer/src/it/bootapp-multi/pom.xml index a4178e8aa..ba12eda08 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-multi/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootapp-multi/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootapp-with-javax/pom.xml b/spring-cloud-function-deployer/src/it/bootapp-with-javax/pom.xml index 8191dd508..c5a375665 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-with-javax/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootapp-with-javax/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootapp-with-scf/pom.xml b/spring-cloud-function-deployer/src/it/bootapp-with-scf/pom.xml index 32125d020..c00c535b3 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-with-scf/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootapp-with-scf/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootapp/pom.xml b/spring-cloud-function-deployer/src/it/bootapp/pom.xml index feb5311a8..4994c90cb 100644 --- a/spring-cloud-function-deployer/src/it/bootapp/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootapp/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootjar-multi/pom.xml b/spring-cloud-function-deployer/src/it/bootjar-multi/pom.xml index ead66cdf4..2e138aa50 100644 --- a/spring-cloud-function-deployer/src/it/bootjar-multi/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootjar-multi/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootjar/pom.xml b/spring-cloud-function-deployer/src/it/bootjar/pom.xml index d20298893..24cd94119 100644 --- a/spring-cloud-function-deployer/src/it/bootjar/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootjar/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml b/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml index 882b60a13..44f6d6f8b 100644 --- a/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java index 730c0b182..2c96fdd64 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionArchiveDeployer.java @@ -20,7 +20,6 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.net.URL; -import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -159,13 +158,6 @@ void undeploy() { } } - // TODO remove this method all together once https://github.com/spring-projects/spring-boot/pull/20851 is addressed - @Override - protected ClassLoader createClassLoader(Iterator archives) throws Exception { - URLClassLoader cl = (URLClassLoader) super.createClassLoader(archives); - return this.createClassLoader(cl.getURLs()); - } - @Override protected ClassLoader createClassLoader(URL[] urls) throws Exception { String classAsPath = DeployerContextUtils.class.getName().replace('.', '/') + ".class"; diff --git a/spring-cloud-function-kotlin/pom.xml b/spring-cloud-function-kotlin/pom.xml index a197f87dc..39f55c59c 100644 --- a/spring-cloud-function-kotlin/pom.xml +++ b/spring-cloud-function-kotlin/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT @@ -64,7 +64,7 @@ kotlin-maven-plugin org.jetbrains.kotlin - 1.6.0 + 2.2.0 -Xjsr305=strict diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinSuspendTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinSuspendTests.java index 53450d2ba..874de12a4 100644 --- a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinSuspendTests.java +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinSuspendTests.java @@ -16,27 +16,16 @@ package org.springframework.cloud.function.kotlin; -import java.lang.reflect.ParameterizedType; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import kotlin.jvm.functions.Function2; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; -import org.springframework.beans.factory.BeanCreationException; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.context.support.GenericApplicationContext; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * @author Adrien Poupard @@ -60,67 +49,29 @@ public void typeDiscoveryTests() { create(new Class[] { KotlinSuspendFlowLambdasConfiguration.class, ContextFunctionCatalogAutoConfigurationKotlinTests.SimpleConfiguration.class }); - Object function = this.context.getBean("kotlinFunction"); - ParameterizedType functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinFunction", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Function.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(2); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - assertThat(functionType.getActualTypeArguments()[1].getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - - function = this.context.getBean("kotlinConsumer"); - functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinConsumer", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Consumer.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(1); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - - function = this.context.getBean("kotlinSupplier"); - functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinSupplier", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Supplier.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(1); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - - function = this.context.getBean("kotlinPojoFunction"); - functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinPojoFunction", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Function.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(2); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - assertThat(functionType.getActualTypeArguments()[1].getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - } - - @Test - public void shouldNotLoadKotlinSuspendLambasNotUsingFlow() { - create(new Class[] { KotlinSuspendLambdasConfiguration.class, - ContextFunctionCatalogAutoConfigurationKotlinTests.SimpleConfiguration.class }); + FunctionCatalog functionCatalog = this.context.getBean(FunctionCatalog.class); - assertThat(this.context.getBean("kotlinFunction")).isInstanceOf(Function2.class); - assertThatThrownBy(() -> { - this.catalog.lookup(Function.class, "kotlinFunction"); - }).isInstanceOf(BeanCreationException.class); + FunctionInvocationWrapper kotlinFunction = functionCatalog.lookup("kotlinFunction"); + assertThat(kotlinFunction.isFunction()).isTrue(); + assertThat(kotlinFunction.getInputType().getTypeName()).isEqualTo("reactor.core.publisher.Flux"); + assertThat(kotlinFunction.getOutputType().getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - assertThatThrownBy(() -> { - this.catalog.lookup(Function.class, "kotlinConsumer"); - }).isInstanceOf(BeanCreationException.class); + FunctionInvocationWrapper kotlinConsumer = functionCatalog.lookup("kotlinConsumer"); + assertThat(kotlinConsumer.isConsumer()).isTrue(); + assertThat(kotlinConsumer.getInputType().getTypeName()).isEqualTo("reactor.core.publisher.Flux"); - assertThatThrownBy(() -> { - this.catalog.lookup(Supplier.class, "kotlinSupplier"); - }).isInstanceOf(BeanCreationException.class); + FunctionInvocationWrapper kotlinSupplier = functionCatalog.lookup("kotlinSupplier"); + assertThat(kotlinSupplier.isSupplier()).isTrue(); + assertThat(kotlinSupplier.getOutputType().getTypeName()).isEqualTo("reactor.core.publisher.Flux"); + FunctionInvocationWrapper kotlinPojoFunction = functionCatalog.lookup("kotlinPojoFunction"); + assertThat(kotlinPojoFunction.isFunction()).isTrue(); + assertThat(kotlinPojoFunction.getInputType().getTypeName()).isEqualTo("reactor.core.publisher.Flux"); + assertThat(kotlinPojoFunction.getOutputType().getTypeName()).isEqualTo("reactor.core.publisher.Flux"); } private void create(Class[] types, String... props) { this.context = (GenericApplicationContext) new SpringApplicationBuilder(types).properties(props).run(); this.catalog = this.context.getBean(FunctionCatalog.class); } - - @EnableAutoConfiguration - @Configuration - protected static class SimpleConfiguration { - - @Bean - public Function function2() { - return value -> value + "function2"; - } - - } - } diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java index d8afa544e..5954bf4c8 100644 --- a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java @@ -16,8 +16,6 @@ package org.springframework.cloud.function.kotlin; -import java.lang.reflect.ParameterizedType; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -57,41 +55,54 @@ public void close() { @Test public void typeDiscoveryTests() { create(new Class[] { KotlinLambdasConfiguration.class, - SimpleConfiguration.class }); - - Object function = this.context.getBean("kotlinFunction"); - ParameterizedType functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinFunction", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Function.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(2); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo(String.class.getName()); - assertThat(functionType.getActualTypeArguments()[1].getTypeName()).isEqualTo(String.class.getName()); - - function = this.context.getBean("kotlinConsumer"); - functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinConsumer", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Consumer.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(1); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo(String.class.getName()); - - function = this.context.getBean("kotlinSupplier"); - functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinSupplier", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Supplier.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(1); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo(String.class.getName()); - - function = this.context.getBean("kotlinPojoFunction"); - functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinPojoFunction", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Function.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(2); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo(Person.class.getName()); - assertThat(functionType.getActualTypeArguments()[1].getTypeName()).isEqualTo(String.class.getName()); - - - function = this.context.getBean("kotlinListPojoFunction"); - functionType = (ParameterizedType) FunctionTypeUtils.discoverFunctionType(function, "kotlinListPojoFunction", this.context); - assertThat(functionType.getRawType().getTypeName()).isEqualTo(Function.class.getName()); - assertThat(functionType.getActualTypeArguments().length).isEqualTo(2); - assertThat(functionType.getActualTypeArguments()[0].getTypeName()).isEqualTo("java.util.List"); - assertThat(functionType.getActualTypeArguments()[1].getTypeName()).isEqualTo(String.class.getName()); + SimpleConfiguration.class, + KotlinComponentFunction.class, + ComponentUppercase.class}); + + FunctionCatalog functionCatalog = this.context.getBean(FunctionCatalog.class); + + FunctionInvocationWrapper kotlinComponentFunction = functionCatalog.lookup("kotlinComponentFunction"); + assertThat(kotlinComponentFunction.isFunction()).isTrue(); + assertThat(kotlinComponentFunction.getInputType().getTypeName()).isEqualTo("java.lang.String"); + assertThat(kotlinComponentFunction.getOutputType().getTypeName()).isEqualTo("java.lang.String"); + assertThat(kotlinComponentFunction.apply("bob")).isEqualTo("BOB"); + + FunctionInvocationWrapper kotlinFunction = functionCatalog.lookup("kotlinFunction"); + assertThat(kotlinFunction.isFunction()).isTrue(); + assertThat(kotlinFunction.getInputType()).isEqualTo(String.class); + assertThat(kotlinFunction.getOutputType()).isEqualTo(String.class); + + FunctionInvocationWrapper kotlinConsumer = functionCatalog.lookup("kotlinConsumer"); + assertThat(kotlinConsumer.isConsumer()).isTrue(); + assertThat(kotlinConsumer.getInputType()).isEqualTo(String.class); + + FunctionInvocationWrapper kotlinSupplier = functionCatalog.lookup("kotlinSupplier"); + assertThat(kotlinSupplier.isSupplier()).isTrue(); + assertThat(kotlinSupplier.getOutputType()).isEqualTo(String.class); + + FunctionInvocationWrapper kotlinPojoFunction = functionCatalog.lookup("kotlinPojoFunction"); + assertThat(kotlinPojoFunction.isFunction()).isTrue(); + assertThat(kotlinPojoFunction.getInputType()).isEqualTo(Person.class); + assertThat(kotlinPojoFunction.getOutputType()).isEqualTo(String.class); + + FunctionInvocationWrapper kotlinListPojoFunction = functionCatalog.lookup("kotlinListPojoFunction"); + assertThat(kotlinListPojoFunction.isFunction()).isTrue(); + assertThat(kotlinListPojoFunction.getInputType().getTypeName()).isEqualTo("java.util.List"); + assertThat(kotlinListPojoFunction.getOutputType()).isEqualTo(String.class); + + FunctionInvocationWrapper componentUppercase = functionCatalog.lookup("componentUppercase"); + assertThat(componentUppercase.isFunction()).isTrue(); + assertThat(componentUppercase.getInputType()).isEqualTo(String.class); + assertThat(componentUppercase.getOutputType()).isEqualTo(String.class); + + assertThat(componentUppercase.apply("hello")).isEqualTo("HELLO"); + + FunctionInvocationWrapper uppercaseBean = functionCatalog.lookup("uppercase"); + assertThat(uppercaseBean.isFunction()).isTrue(); + assertThat(uppercaseBean.getInputType()).isEqualTo(String.class); + assertThat(uppercaseBean.getOutputType()).isEqualTo(String.class); + + assertThat(uppercaseBean.apply("hello")).isEqualTo("HELLO"); } @Test diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/ComponentUppercase.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/ComponentUppercase.kt new file mode 100644 index 000000000..574b6aa48 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/ComponentUppercase.kt @@ -0,0 +1,10 @@ +package org.springframework.cloud.function.kotlin + +import org.springframework.stereotype.Component + +@Component +class ComponentUppercase : (String) -> String { + override fun invoke(p1: String): String { + return p1.uppercase() + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentFunction.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentFunction.kt new file mode 100644 index 000000000..629f981ff --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentFunction.kt @@ -0,0 +1,12 @@ +package org.springframework.cloud.function.kotlin + +import org.springframework.stereotype.Component +import java.util.function.Function + +@Component +class KotlinComponentFunction : Function { + + override fun apply(t: String): String { + return t.uppercase(); + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt index 9e85d664b..ef4583d39 100644 --- a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt @@ -29,6 +29,9 @@ import java.util.List @EnableAutoConfiguration @Configuration class KotlinLambdasConfiguration { + + @Bean + fun uppercase(): Function = KotlinComponentFunction() @Bean fun kotlinFunction(): (String) -> String { return { it.toUpperCase() } diff --git a/spring-cloud-function-rsocket/pom.xml b/spring-cloud-function-rsocket/pom.xml index 60cc3ea56..46c0ed23b 100644 --- a/spring-cloud-function-rsocket/pom.xml +++ b/spring-cloud-function-rsocket/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-azure-di-samples/README.md b/spring-cloud-function-samples/function-azure-di-samples/README.md new file mode 100644 index 000000000..0a1c6b77f --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/README.md @@ -0,0 +1,197 @@ +# Azure Functions with DI adapter + +## Common instructions to integrate Azure Functions with Spring Framework + +* Use the [Spring Initializer](https://start.spring.io/) to generate a pain, java Spring Boot project without additional dependencies. Set the boot version to `2.7.x`, the build to `Maven` and the packaging to `Jar`. + +* Add the `spring-cloud-function-adapter-azure` POM dependency: + + ```xml + + org.springframework.cloud + spring-cloud-function-adapter-azure + 3.2.9 or higher + + ``` + Having the adapter on the classpath activates the Azure Java Worker integration. + +* Implement the [Azure Java Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#java-function-basics) as `@FunctionName` annotated methods: + + ```java + import java.util.Optional; + import java.util.function.Function; + + import com.microsoft.azure.functions.ExecutionContext; + import com.microsoft.azure.functions.HttpMethod; + import com.microsoft.azure.functions.HttpRequestMessage; + import com.microsoft.azure.functions.annotation.AuthorizationLevel; + import com.microsoft.azure.functions.annotation.FunctionName; + import com.microsoft.azure.functions.annotation.HttpTrigger; + + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.stereotype.Component; + + @Component + public class MyAzureFunction { + + @Autowired + private Function uppercase; + + @FunctionName("ditest") + public String execute( + @HttpTrigger(name = "req", methods = { HttpMethod.GET, + HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + ExecutionContext context) { + + return this.uppercase.apply(request.getBody().get()); + } + } + ``` + - The `@FunctionName` annotated methods represent the Azure Function implementations. + - The class must be marked with the Spring `@Component` annotation. + - You can use any Spring mechanism to auto-wire the Spring beans used for the function implementation. + +* Add the `host.json` configuration under the `src/main/resources` folder: + + ```json + { + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } + } + ``` + +* When bootstrapped as Spring Boot project make sure to either disable the `spring-boot-maven-plugin` plugin or cover it into `thin-layout`: + + ```xml + + org.springframework.boot + spring-boot-maven-plugin + + + org.springframework.boot.experimental + spring-boot-thin-layout + ${spring-boot-thin-layout.version} + + + + ``` + Since Azure Functions requires a specific, custom, Jar packaging we have to disable SpringBoot one. + +* Add the `azure-functions-maven-plugin` to your POM configuration. A sample configuration would look like this. + + ```xml + + com.microsoft.azure + azure-functions-maven-plugin + 1.22.0 or higher + + + YOUR-AZURE-FUNCTION-APP-NAME + YOUR-AZURE-FUNCTION-RESOURCE-GROUP + YOUR-AZURE-FUNCTION-APP-REGION + YOUR-AZURE-FUNCTION-APP-SERVICE-PLANE-NAME + YOUR-AZURE-FUNCTION-PRICING-TIER + + ${project.basedir}/src/main/resources/host.json + + + linux + 11 + + + 7072 + + + + FUNCTIONS_EXTENSION_VERSION + ~4 + + + + + + package-functions + + package + + + + + ``` + - Set the AZURE subscription configuration such as app name, resource group, region, service plan, pricing Tier + - Runtime configuration: + - [Java Versions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#java-versions) + - Specify [Deployment OS](https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#specify-the-deployment-os) + +* Build the project: + + ``` + ./mvnw clean package + ``` + +## Running Locally + +NOTE: To run locally on top of `Azure Functions`, and to deploy to your live Azure environment, you will need `Azure Functions Core Tools` installed along with the Azure CLI (see [here](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-java?tabs=bash%2Cazure-cli%2Cbrowser#configure-your-local-environment)). +For some configuration you would need the [Azurite emulator](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-emulator) as well. + +Then build and run the sample: + +``` +./mvnw clean package +./mvnw azure-functions:run +``` + +## Running on Azure + +Make sure you are logged in your Azure account. +``` +az login +``` + +Build and deploy + +``` +./mvnw clean package +./mvnw azure-functions:deploy +``` + +## Debug locally + +Run the function in debug mode. +``` +./mvnw azure-functions:deploy -DenableDebug +``` + +Alternatively and the `JAVA_OPTS` value to your `local.settings.json` like this: + +```json +{ + "IsEncrypted": false, + "Values": { + ... + "FUNCTIONS_WORKER_RUNTIME": "java", + "JAVA_OPTS": "-Djava.net.preferIPv4Stack=true -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=127.0.0.1:5005" + } +} +``` + + +VS Code remote debug configuration: + + ```json + { + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Attach to Remote Program", + "request": "attach", + "hostName": "localhost", + "port": "5005" + }, + } + + ``` \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.gitignore b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.mvn/wrapper/maven-wrapper.jar b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..c1dd12f17 Binary files /dev/null and b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.mvn/wrapper/maven-wrapper.jar differ diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.mvn/wrapper/maven-wrapper.properties b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..b74bf7fcd --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/README.md b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/README.md new file mode 100644 index 000000000..37bb86dc6 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/README.md @@ -0,0 +1,6 @@ + + +The Blob storage binding is part of an [extension bundle](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-register#extension-bundles), which is specified in your [host.json](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-blob?tabs=in-process%2Cextensionv5%2Cextensionv3&pivots=programming-language-java#install-bundle) project file. + + +The Blob storage trigger starts a function when a new or updated blob is detected. The blob contents are provided as [input](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-blob-input?tabs=in-process%2Cextensionv5&pivots=programming-language-java) to the function. \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/mvnw b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/mvnw new file mode 100755 index 000000000..8a8fb2282 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/mvnw.cmd b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/mvnw.cmd new file mode 100644 index 000000000..1d8ab018e --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/pom.xml b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/pom.xml new file mode 100644 index 000000000..096a1a27a --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.15 + + + + com.example.azure.di + azure-blob-trigger-demo + 1.0.0 + azure-blob-trigger-demo + Demo project for Spring Boot + + 11 + 1.0.28.RELEASE + + com.example.azure.di.azureblobtriggerdemo.AzureBlobTriggerDemoApplication + + + 1.22.0 + spring-cloud-function-samples + westus + java-functions-group + java-functions-app-service-plan + EP1 + + + + + org.springframework.cloud + spring-cloud-function-adapter-azure + 3.2.9 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + com.microsoft.azure + azure-functions-maven-plugin + ${azure.functions.maven.plugin.version} + + + ${functionAppName} + ${functionResourceGroup} + ${functionAppRegion} + ${functionAppServicePlanName} + ${functionPricingTier} + + ${project.basedir}/src/main/resources/host.json + ${project.basedir}/src/main/resources/local.settings.json + + + linux + 11 + + + 7072 + + + + FUNCTIONS_EXTENSION_VERSION + ~4 + + + + + + package-functions + + package + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.springframework.boot.experimental + spring-boot-thin-layout + ${spring-boot-thin-layout.version} + + + + + + + diff --git a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerApplication.java b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplication.java similarity index 64% rename from spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerApplication.java rename to spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplication.java index 1997ad6d2..fe209a965 100644 --- a/spring-cloud-function-compiler/src/main/java/org/springframework/cloud/function/compiler/app/CompilerApplication.java +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplication.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2021-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,18 +14,23 @@ * limitations under the License. */ -package org.springframework.cloud.function.compiler.app; +package com.example.azure.di.azureblobtriggerdemo; + +import java.util.function.Function; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; -// @checkstyle:off @SpringBootApplication -public class CompilerApplication { +public class AzureBlobTriggerDemoApplication { public static void main(String[] args) { - SpringApplication.run(CompilerApplication.class, args); + SpringApplication.run(AzureBlobTriggerDemoApplication.class, args); } + @Bean + public Function uppercase() { + return payload -> payload.toUpperCase(); + } } -// @checkstyle:on diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/java/com/example/azure/di/azureblobtriggerdemo/MyBlobFunction.java b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/java/com/example/azure/di/azureblobtriggerdemo/MyBlobFunction.java new file mode 100644 index 000000000..302e07e34 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/java/com/example/azure/di/azureblobtriggerdemo/MyBlobFunction.java @@ -0,0 +1,65 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.azure.di.azureblobtriggerdemo; + +import java.util.function.Function; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.OutputBinding; +import com.microsoft.azure.functions.annotation.BindingName; +import com.microsoft.azure.functions.annotation.BlobInput; +import com.microsoft.azure.functions.annotation.BlobOutput; +import com.microsoft.azure.functions.annotation.BlobTrigger; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.StorageAccount; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Azure Functions with Azure Storage Blob. + * https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-storage-blob-trigger?tabs=java + * + * The Blob storage binding is part of an extension bundle, which is specified in your host.json project file. + */ + +@Component +public class MyBlobFunction { + + + @Autowired + private Function uppercase; + + /** + * This function will be invoked when a new or updated blob is detected at the specified path. The blob contents are + * provided as input to this function. The location of the blob is provided in the path parameter. Example - + * test-triggerinput-java/{name} below + */ + @FunctionName("BlobTrigger") + @StorageAccount("AzureWebJobsStorage") + public void BlobTriggerToBlobTest( + @BlobTrigger(name = "triggerBlob", path = "test-triggerinput-java/{name}", dataType = "binary") byte[] triggerBlob, + @BindingName("name") String fileName, + @BlobInput(name = "inputBlob", path = "test-input-java/{name}", dataType = "binary") byte[] inputBlob, + @BlobOutput(name = "outputBlob", path = "test-output-java/{name}", dataType = "binary") OutputBinding outputBlob, + final ExecutionContext context) { + + context.getLogger().info("Java Blob trigger function BlobTriggerToBlobTest processed a blob.\n Name: " + + fileName + "\n Size: " + triggerBlob.length + " Bytes"); + outputBlob.setValue(inputBlob); + } +} diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/application.properties b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/host.json b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/host.json new file mode 100644 index 000000000..10d0c0748 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/local.settings.json b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/local.settings.json new file mode 100644 index 000000000..adce8b884 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/main/resources/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "AzureWebJobsDashboard": "", + "FUNCTIONS_WORKER_RUNTIME": "java" + } +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/test/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplicationTests.java b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/test/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplicationTests.java new file mode 100644 index 000000000..2b9b92ad5 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-blob-trigger-demo/src/test/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.azure.di.azureblobtriggerdemo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AzureBlobTriggerDemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.gitignore b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.mvn/wrapper/maven-wrapper.jar b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..c1dd12f17 Binary files /dev/null and b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.mvn/wrapper/maven-wrapper.jar differ diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.mvn/wrapper/maven-wrapper.properties b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..b74bf7fcd --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/mvnw b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/mvnw new file mode 100755 index 000000000..8a8fb2282 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/mvnw.cmd b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/mvnw.cmd new file mode 100644 index 000000000..1d8ab018e --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/pom.xml b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/pom.xml new file mode 100644 index 000000000..0da27bbe8 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/pom.xml @@ -0,0 +1,104 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.15 + + + + com.example.azure.di + azure-httptrigger-demo + 1.0.0 + azure-httptrigger-demo + + Demo Spring Boot, Azure Function - HttpTrigger (DI adapter) + + + 11 + 1.0.28.RELEASE + + + com.example.azure.di.httptriggerdemo.HttpTriggerDemoApplication + + + 1.22.0 + spring-cloud-function-samples + westus + java-functions-group + java-functions-app-service-plan + EP1 + + + + + org.springframework.cloud + spring-cloud-function-adapter-azure + 3.2.9 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + + com.microsoft.azure + azure-functions-maven-plugin + ${azure.functions.maven.plugin.version} + + + ${functionAppName} + ${functionResourceGroup} + ${functionAppRegion} + ${functionAppServicePlanName} + ${functionPricingTier} + + ${project.basedir}/src/main/resources/host.json + + + linux + 11 + + + 7072 + + + + FUNCTIONS_EXTENSION_VERSION + ~4 + + + + + + package-functions + + package + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.springframework.boot.experimental + spring-boot-thin-layout + ${spring-boot-thin-layout.version} + + + + + + + diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/java/com/example/azure/di/httptriggerdemo/HttpTriggerDemoApplication.java b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/java/com/example/azure/di/httptriggerdemo/HttpTriggerDemoApplication.java new file mode 100644 index 000000000..d462f8c97 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/java/com/example/azure/di/httptriggerdemo/HttpTriggerDemoApplication.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.azure.di.httptriggerdemo; + +import java.util.function.Function; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +@SpringBootApplication +public class HttpTriggerDemoApplication { + + @Bean + public Function echo() { + return payload -> payload; + } + + @Bean + public Function uppercase() { + return payload -> payload.toUpperCase(); + } + + public static void main(String[] args) { + SpringApplication.run(HttpTriggerDemoApplication.class, args); + } + +} diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/java/com/example/azure/di/httptriggerdemo/MyAzureFunction.java b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/java/com/example/azure/di/httptriggerdemo/MyAzureFunction.java new file mode 100644 index 000000000..0bcf1879d --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/java/com/example/azure/di/httptriggerdemo/MyAzureFunction.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.azure.di.httptriggerdemo; + +import java.util.Optional; +import java.util.function.Function; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class MyAzureFunction { + + @Autowired + private Function echo; + + @Autowired + private Function uppercase; + + @FunctionName("ditest") + public String execute( + @HttpTrigger(name = "req", methods = { HttpMethod.GET, + HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + ExecutionContext context) { + + return echo.andThen(uppercase).apply(request.getBody().get()); + } +} diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/resources/application.properties b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/resources/host.json b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/resources/host.json new file mode 100644 index 000000000..10d0c0748 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/main/resources/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/test/java/com/example/azure/di/httptriggerdemo/HttptriggerDemoApplicationTests.java b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/test/java/com/example/azure/di/httptriggerdemo/HttptriggerDemoApplicationTests.java new file mode 100644 index 000000000..dbc76d294 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-httptrigger-demo/src/test/java/com/example/azure/di/httptriggerdemo/HttptriggerDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.azure.di.httptriggerdemo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HttptriggerDemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.gitignore b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.gitignore new file mode 100644 index 000000000..549e00a2a --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.mvn/wrapper/maven-wrapper.jar b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..c1dd12f17 Binary files /dev/null and b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.mvn/wrapper/maven-wrapper.jar differ diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.mvn/wrapper/maven-wrapper.properties b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..b74bf7fcd --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/README.md b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/README.md new file mode 100644 index 000000000..5b459e447 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/README.md @@ -0,0 +1,110 @@ +# Azure TimerTrigger Function + +Spring Cloud Function example for implementing [Timer trigger for Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-java). + +## Running Locally + +NOTE: To run locally on top of `Azure Functions`, and to deploy to your live Azure environment, you will need `Azure Functions Core Tools` installed along with the Azure CLI (see [here](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-java?tabs=bash%2Cazure-cli%2Cbrowser#configure-your-local-environment)) as well as the Use [Azurite emulator](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-emulator) for local Azure Storage development. For the emulator you can run a docker container (see below) or use the [Visual-Studio-Code extension](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio-code). + +Here is how ot start the `Azure emulator` as docker container: + +``` +docker run --name azurite --rm -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite +``` + +Then build and run the sample: + +``` +./mvnw clean package +./mvnw azure-functions:run +``` + +The timer triggers the function every minute. +In result the the `uppercase` Spring Cloud Function is called and uppercase the timeInfo and logs it into the context. + +``` +[2022-10-11T08:53:00.011Z] Execution Context Log - TimeInfo: {"Schedule":{"AdjustForDST":true},"ScheduleStatus":{"Last":"2022-10-11T10:52:00.003967+02:00","Next":"2022-10-11T10:53:00+02:00","LastUpdated":"2022-10-11T10:52:00.003967+02:00"},"IsPastDue":false} +``` + +## Running on Azure + +Make sure you are logged in your Azure account. +``` +az login +``` + +Build and deploy + +``` +./mvnw clean package +./mvnw azure-functions:deploy +``` + +## Implementation details + +The `spring-cloud-function-adapter-azure` dependency activates the AzureFunctionInstanceInjector: + +```xml + + org.springframework.cloud + spring-cloud-function-adapter-azure + 3.2.9-SNAPSHOT + +``` + +(Version 3.2.9 or higher) + + +The `uppercase` function with signature `Function, Void> uppercase()` is defined as `@Bean` in the TimeTriggerDemoApplication context. + + +```java + @Bean + public Consumer> uppercase() { + return message -> { + String timeInfo = message.getPayload(); + String value = timeInfo.toUpperCase(); + + logger.info("Timer is triggered with TimeInfo: " + value); + + // (Optionally) access and use the Azure function context. + ExecutionContext context = (ExecutionContext) message.getHeaders().get(UppercaseHandler.EXECUTION_CONTEXT); + context.getLogger().info("Execution Context Log - TimeInfo: " + value); + + // No response. + }; + } +``` + +TIP: The uppercase function does not return value (e.g. Void output type) and is backed by `java.util.Consumer`. + +The `UppercaseHandler` (marked as Spring `@Component`) implements the Azure function using the Azure Function Java API. Furthermore as Spring component the UppercaseHandler leverages the Spring configuration and programming model to inject the necessary services required by the functions. + +```java +@Component +public class UppercaseHandler { + + public static String EXECUTION_CONTEXT = "executionContext"; + + @Autowired + private Consumer> uppercase; + + @FunctionName("uppercase") + public void execute(@TimerTrigger(name = "keepAliveTrigger", schedule = "0 */1 * * * *") String timerInfo, + ExecutionContext context) { + + Message message = MessageBuilder + .withPayload(timerInfo) + .setHeader(EXECUTION_CONTEXT, context) + .build(); + + this.uppercase.accept(message); + } +} +``` + + +## Notes + +* Change the `spring-boot-maven-plugin` to `tiny` in favor of the `azure-functions-maven-plugin` jar packaging. +* Add `"AzureWebJobsStorage": "UseDevelopmentStorage=true"` to the `local.settings.json`. diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/mvnw b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/mvnw new file mode 100755 index 000000000..8a8fb2282 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/mvnw.cmd b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/mvnw.cmd new file mode 100644 index 000000000..1d8ab018e --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/pom.xml b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/pom.xml new file mode 100644 index 000000000..87b3f00fb --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.15 + + + + com.example.azure.di + azure-timetrigger-demo + 1.0.0 + azure-timetrigger-demo + Demo project for Spring Boot + + 11 + 1.0.28.RELEASE + + + com.example.azure.di.timetriggerdemo.TimeTriggerDemoApplication + + + 1.22.0 + spring-cloud-function-samples + westus + java-functions-group + java-functions-app-service-plan + EP1 + + + + + org.springframework.cloud + spring-cloud-function-adapter-azure + 3.2.9 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + com.microsoft.azure + azure-functions-maven-plugin + ${azure.functions.maven.plugin.version} + + + ${functionAppName} + ${functionResourceGroup} + ${functionAppRegion} + ${functionAppServicePlanName} + ${functionPricingTier} + + ${project.basedir}/src/main/resources/host.json + ${project.basedir}/src/main/resources/local.settings.json + + + linux + 11 + + + 7072 + + + + FUNCTIONS_EXTENSION_VERSION + ~4 + + + + + + package-functions + + package + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.springframework.boot.experimental + spring-boot-thin-layout + ${spring-boot-thin-layout.version} + + + + + + + diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/java/com/example/azure/di/timetriggerdemo/TimeTriggerDemoApplication.java b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/java/com/example/azure/di/timetriggerdemo/TimeTriggerDemoApplication.java new file mode 100644 index 000000000..e3e10ddc8 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/java/com/example/azure/di/timetriggerdemo/TimeTriggerDemoApplication.java @@ -0,0 +1,55 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.azure.di.timetriggerdemo; + +import java.util.function.Consumer; + +import com.microsoft.azure.functions.ExecutionContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.Message; + +@SpringBootApplication +public class TimeTriggerDemoApplication { + + private static Log logger = LogFactory.getLog(TimeTriggerDemoApplication.class); + + public static void main(String[] args) { + SpringApplication.run(TimeTriggerDemoApplication.class, args); + } + + @Bean + public Consumer> uppercase() { + return message -> { + String timeInfo = message.getPayload(); + String value = timeInfo.toUpperCase(); + + logger.info("Timer is triggered with TimeInfo: " + value); + + // (Optionally) access and use the Azure function context. + ExecutionContext context = (ExecutionContext) message.getHeaders().get(UppercaseHandler.EXECUTION_CONTEXT); + context.getLogger().info("Execution Context Log - TimeInfo: " + value); + + // No response. + }; + } + +} diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/java/com/example/azure/di/timetriggerdemo/UppercaseHandler.java b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/java/com/example/azure/di/timetriggerdemo/UppercaseHandler.java new file mode 100644 index 000000000..4e217edf6 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/java/com/example/azure/di/timetriggerdemo/UppercaseHandler.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.azure.di.timetriggerdemo; + +import java.util.function.Consumer; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.TimerTrigger; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; + +@Component +public class UppercaseHandler { + + public static String EXECUTION_CONTEXT = "executionContext"; + + @Autowired + private Consumer> uppercase; + + @FunctionName("uppercase") + public void execute(@TimerTrigger(name = "keepAliveTrigger", schedule = "0 */1 * * * *") String timerInfo, + ExecutionContext context) { + + Message message = MessageBuilder + .withPayload(timerInfo) + .setHeader(EXECUTION_CONTEXT, context) + .build(); + + this.uppercase.accept(message); + } +} diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/application.properties b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/host.json b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/host.json new file mode 100644 index 000000000..10d0c0748 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/host.json @@ -0,0 +1,7 @@ +{ + "version": "2.0", + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[3.*, 4.0.0)" + } +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/local.settings.json b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/local.settings.json new file mode 100644 index 000000000..adce8b884 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/main/resources/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "AzureWebJobsDashboard": "", + "FUNCTIONS_WORKER_RUNTIME": "java" + } +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/test/java/com/example/azure/di/timetriggerdemo/TimetriggerDemoApplicationTests.java b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/test/java/com/example/azure/di/timetriggerdemo/TimetriggerDemoApplicationTests.java new file mode 100644 index 000000000..d78399a12 --- /dev/null +++ b/spring-cloud-function-samples/function-azure-di-samples/azure-timetrigger-demo/src/test/java/com/example/azure/di/timetriggerdemo/TimetriggerDemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.azure.di.timetriggerdemo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TimetriggerDemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/spring-cloud-function-samples/function-functional-sample-aws/pom.xml b/spring-cloud-function-samples/function-functional-sample-aws/pom.xml index 858da0ed6..5bdb68a6f 100644 --- a/spring-cloud-function-samples/function-functional-sample-aws/pom.xml +++ b/spring-cloud-function-samples/function-functional-sample-aws/pom.xml @@ -15,7 +15,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -25,7 +25,7 @@ 1.8 1.0.27.RELEASE 3.9.0 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT @@ -33,11 +33,6 @@ org.springframework.cloud spring-cloud-function-adapter-aws - - org.springframework.cloud - spring-cloud-function-web - - com.amazonaws aws-lambda-java-events @@ -96,7 +91,6 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 false true diff --git a/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/FunctionConfiguration.java b/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/FunctionConfiguration.java index 34ff8b4d4..eca9968fe 100644 --- a/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/FunctionConfiguration.java +++ b/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/FunctionConfiguration.java @@ -8,6 +8,9 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.support.GenericApplicationContext; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + @SpringBootConfiguration public class FunctionConfiguration implements ApplicationContextInitializer { @@ -26,5 +29,9 @@ public void initialize(GenericApplicationContext context) { context.registerBean("uppercase", FunctionRegistration.class, () -> new FunctionRegistration<>(function).type( FunctionType.from(String.class).to(String.class))); + + context.registerBean("testFunction", FunctionRegistration.class, + () -> new FunctionRegistration<>(new TestFunction()).type( + FunctionType.from(APIGatewayProxyRequestEvent.class).to(APIGatewayProxyResponseEvent.class))); } } diff --git a/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/TestFunction.java b/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/TestFunction.java new file mode 100644 index 000000000..b097d0ad5 --- /dev/null +++ b/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/TestFunction.java @@ -0,0 +1,13 @@ +package example; + +import java.util.function.Function; + +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +public class TestFunction implements Function { + @Override + public APIGatewayProxyResponseEvent apply(APIGatewayProxyRequestEvent apiGatewayProxyRequestEvent) { + return new APIGatewayProxyResponseEvent().withStatusCode(200).withBody("ok"); + } +} diff --git a/spring-cloud-function-samples/function-sample-aws-custom-bean/pom.xml b/spring-cloud-function-samples/function-sample-aws-custom-bean/pom.xml index 7f15c554d..76955b941 100644 --- a/spring-cloud-function-samples/function-sample-aws-custom-bean/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws-custom-bean/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 io.spring.sample @@ -17,7 +17,7 @@ 1.8 1.0.27.RELEASE - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-aws-custom/pom.xml b/spring-cloud-function-samples/function-sample-aws-custom/pom.xml index fc70937b9..415c36bbe 100644 --- a/spring-cloud-function-samples/function-sample-aws-custom/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws-custom/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 io.spring.sample @@ -17,7 +17,7 @@ 1.8 1.0.27.RELEASE - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-aws-custom/src/main/java/com/example/LambdaApplication.java b/spring-cloud-function-samples/function-sample-aws-custom/src/main/java/com/example/LambdaApplication.java index 730053793..cea0dfab7 100644 --- a/spring-cloud-function-samples/function-sample-aws-custom/src/main/java/com/example/LambdaApplication.java +++ b/spring-cloud-function-samples/function-sample-aws-custom/src/main/java/com/example/LambdaApplication.java @@ -21,6 +21,9 @@ public class LambdaApplication public Function uppercase() { return value -> { logger.info("Processing: " + value); + if (value.equals("error")) { + throw new IllegalArgumentException("Intentional"); + } return value.toUpperCase(); }; } diff --git a/spring-cloud-function-samples/function-sample-aws-routing/pom.xml b/spring-cloud-function-samples/function-sample-aws-routing/pom.xml index 3e0304a30..8e5ec30fd 100644 --- a/spring-cloud-function-samples/function-sample-aws-routing/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws-routing/pom.xml @@ -15,7 +15,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -25,7 +25,7 @@ 1.8 1.0.27.RELEASE 2.0.2 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-aws/README.adoc b/spring-cloud-function-samples/function-sample-aws/README.adoc new file mode 100644 index 000000000..44ec0fb3e --- /dev/null +++ b/spring-cloud-function-samples/function-sample-aws/README.adoc @@ -0,0 +1,59 @@ +== Introduction + +Spring Cloud Function is a framework that promotes the notion that any business requirement could be realized as one of the `java.util.Supplier`, `java.util.Function` or `java.util.Consumer` while providing a set of abstractions to execute such functions in different context using provided set of Spring Boot adapter auto-configurations. For example, the same function implementation could be realized as Message Handler or REST endpoint or AWS Lambda function and many more without touching its code, by simply adding one of the provided dependencies to your POM or Gradle script. + +While you can find more details on Spring Cloud Function and all of its features https://spring.io/projects/spring-cloud-function[here], this example is about AWS Lambda functions and Spring support for it. + +While AWS provides a set of interfaces to implement various type of handlers (`RequestHandler`, `RequestStreamHandler` etc), you may want to keep your implementations POJO-like or you may have an existing legacy code that you would want to turn into AWS lambda. Regardless of the case Spring Cloud Function AWS adapter can help. +This example demonstrates how a simple Java Function managed as Spring Bean can be easily turned into and deployed as AWS Lambda function. + +=== Basic Example + +The example is very trivial, so here is the code: + +[source, java] +---- +@SpringBootApplication +public class FunctionConfiguration { + + @Bean + public Function uppercase() { + return value -> value.toUpperCase(); + } +} +---- + +As you can see it's a standard Spring Boot application that has no code-level dependencies on AWS. In fact outside of Spring configuration annotations such as `@Bean` and `@SpringBootApplication`, it has no code-level dependencies on Spring either. While implementation of this function is trivial, you can imagine a more involved example with complex types, callbacks to legacy code etc (see <> for references to such samples). + +So, once deployed as AWS Lambda you can send a String and receive the uppercased version of it back. + +**Build** + +[source, text] +---- +./mvnw clean package +---- + +**Deploy** + +Combination of pre-configured https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/[Spring Boot maven plugin] and https://maven.apache.org/plugins/maven-shade-plugin/[Apache Maven Shade plugin] will produce a deployable JAR file. + + +NOTE: _Since the intention of this example to be as trivial as possible we do not provide a deploy script (i.e., https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html[SAM]) to ensure there is nothing else that needs to be learned if this is read by a new AWS user, and instead recommend using AWS Dashboard functionality to deploy and test it. Some of the more complex examples listed in the next section have deploy scripts to simplify the process._ + +Now all you need to do is deploy (upload) the JAR file located in the `target` directory of the project to AWS. + +NOTE: You will find two JAR files (actually three where one has additional extension `.original`). The AWS deployable artifact is the one that ends with `-aws.jar`. For example, in our case it's `function-sample-aws-2.0.0.RELEASE-aws.jar`. + +When ask about _handler_ you specify `org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest` which is a generic request handler provided by the Spring Cloud Function. +You can also find more details deployment procedures in this quick https://docs.spring.io/spring-cloud-function/docs/3.2.9/reference/html/aws.html#_getting_started[getting started] paragraph available in Spring Cloud Function documentation. + +=== Other Examples + +Below is a set of additional examples showcasing more complex scenarios + +- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-functional-sample-aws[Functional Bean registration]. This example is similar to the basic example provided above with the exception that it demonstrates https://docs.spring.io/spring-cloud-function/docs/3.2.9/reference/html/spring-cloud-function.html#_functional_bean_definitions[functional bean registration] instead of classic which greatly improves startup time. +- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-custom-bean[AWS Custom Runtime - classic Spring bean registration]. This example demonstrates usage of AWS custom runtime with classic bean registration. +- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-custom[AWS Custom Runtime - functional Spring bean registration]. This example demonstrates usage of AWS custom runtime with functional bean registration. +- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-native[GraalVM Native in AWS]. This example demonstrates Spring Cloud Function support for GraalVM Native deployment. +- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[Function Routing with AWS]. This example demonstrates routing capabilities of Spring Cloud Function when deployed as AWS Lambdas diff --git a/spring-cloud-function-samples/function-sample-aws/build.gradle b/spring-cloud-function-samples/function-sample-aws/build.gradle index a641c315f..d72a2d3f4 100644 --- a/spring-cloud-function-samples/function-sample-aws/build.gradle +++ b/spring-cloud-function-samples/function-sample-aws/build.gradle @@ -1,7 +1,7 @@ buildscript { ext { - springBootVersion = '2.2.0.BUILD-SNAPSHOT' - wrapperVersion = '1.0.17.RELEASE' + springBootVersion = '2.6.7' + wrapperVersion = '1.0.29.BUILD-SNAPSHOT' shadowVersion = '5.1.0' } repositories { @@ -20,7 +20,7 @@ buildscript { } apply plugin: 'java' -apply plugin: 'maven' +apply plugin: 'maven-publish' apply plugin: 'eclipse' apply plugin: 'com.github.johnrengelman.shadow' apply plugin: 'org.springframework.boot' @@ -40,8 +40,8 @@ repositories { } ext { - springCloudFunctionVersion = "3.0.0.BUILD-SNAPSHOT" - awsLambdaEventsVersion = "2.0.2" + springCloudFunctionVersion = "3.2.4" + awsLambdaEventsVersion = "3.9.0" awsLambdaCoreVersion = "1.1.0" } ext['reactor.version'] = "3.1.7.RELEASE" @@ -84,10 +84,10 @@ dependencyManagement { } dependencies { - compile("org.springframework.cloud:spring-cloud-function-adapter-aws") - compile("org.springframework.cloud:spring-cloud-starter-function-webflux") - compile("org.springframework.boot:spring-boot-configuration-processor") + implementation("org.springframework.cloud:spring-cloud-function-adapter-aws") + implementation("org.springframework.cloud:spring-cloud-starter-function-webflux") + implementation("org.springframework.boot:spring-boot-configuration-processor") compileOnly("com.amazonaws:aws-lambda-java-events:${awsLambdaEventsVersion}") compileOnly("com.amazonaws:aws-lambda-java-core:${awsLambdaCoreVersion}") testCompile('org.springframework.boot:spring-boot-starter-test') -} +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-aws/pom.xml b/spring-cloud-function-samples/function-sample-aws/pom.xml index 4653b95c5..9abaa9c54 100644 --- a/spring-cloud-function-samples/function-sample-aws/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws/pom.xml @@ -15,7 +15,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -25,7 +25,7 @@ 1.8 1.0.27.RELEASE 3.9.0 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT @@ -33,15 +33,6 @@ org.springframework.cloud spring-cloud-function-adapter-aws - - org.springframework.cloud - spring-cloud-function-web - - - org.springframework.boot - spring-boot-starter-web - - com.amazonaws aws-lambda-java-events diff --git a/spring-cloud-function-samples/function-sample-aws/src/main/java/example/FunctionConfiguration.java b/spring-cloud-function-samples/function-sample-aws/src/main/java/example/FunctionConfiguration.java index ade13c608..eb9e08254 100644 --- a/spring-cloud-function-samples/function-sample-aws/src/main/java/example/FunctionConfiguration.java +++ b/spring-cloud-function-samples/function-sample-aws/src/main/java/example/FunctionConfiguration.java @@ -19,13 +19,6 @@ public static void main(String[] args) { @Bean public Function uppercase() { - return value -> { - if (value.equals("exception")) { - throw new RuntimeException("Intentional exception"); - } - else { - return value.toUpperCase(); - } - }; + return value -> value.toUpperCase(); } } diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/.gitignore b/spring-cloud-function-samples/function-sample-azure-timer-trigger/.gitignore new file mode 100644 index 000000000..7ed0d6b67 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/.gitignore @@ -0,0 +1,32 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/.mvn/wrapper/maven-wrapper.jar b/spring-cloud-function-samples/function-sample-azure-timer-trigger/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 000000000..c1dd12f17 Binary files /dev/null and b/spring-cloud-function-samples/function-sample-azure-timer-trigger/.mvn/wrapper/maven-wrapper.jar differ diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/.mvn/wrapper/maven-wrapper.properties b/spring-cloud-function-samples/function-sample-azure-timer-trigger/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 000000000..b74bf7fcd --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/README.md b/spring-cloud-function-samples/function-sample-azure-timer-trigger/README.md new file mode 100644 index 000000000..af856c669 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/README.md @@ -0,0 +1,93 @@ +# Azure TimerTrigger Function + +Spring Cloud Function example for implementing [Timer trigger for Azure Functions](https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-java). + +NOTE: JVM '17' is required. + +https://learn.microsoft.com/en-us/azure/azure-functions/functions-bindings-timer?tabs=in-process&pivots=programming-language-java + +## Running Locally + +NOTE: To run locally on top of Azure Functions, and to deploy to your live Azure environment, you will need Azure Functions Core Tools installed along with the Azure CLI (see [here](https://docs.microsoft.com/en-us/azure/azure-functions/create-first-function-cli-java?tabs=bash%2Cazure-cli%2Cbrowser#configure-your-local-environment) for details) as well as the Use [Azurite emulator](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-emulator) for local Azure Storage development. For the emulator you can run a docker container (see below) or use the [Visual-Studio-Code extension](https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?tabs=visual-studio-code). + +``` +docker run --name azurite --rm -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite +``` + +``` +./mvnw clean package +./mvnw azure-functions:run +``` + +The timer triggers the function every minute. +In result the the `uppercase` Spring Cloud Function is called and uppercase the timeInfo and logs it into the context. + +``` +[2022-10-11T08:53:00.011Z] Timer is triggered: {"Schedule":{"AdjustForDST":true},"ScheduleStatus":{"Last":"2022-10-11T10:52:00.003967+02:00","Next":"2022-10-11T10:53:00+02:00","LastUpdated":"2022-10-11T10:52:00.003967+02:00"},"IsPastDue":false} +``` + +## Running on Azure + +Make sure you are logged in your Azure account. +``` +az login +``` + +Build and deploy + +``` +./mvnw clean package +./mvnw azure-functions:deploy +``` + +## Implementation details + +The `uppercase` function signature is `Function, Void> uppercase()`. The implementation of `UppercaseHandler` (which extends `FunctionInvoker`) provides access to the Azure Function context via the _MessageHeaders_. + +NOTE: Implementation of `FunctionInvoker` (your handler), should contain the least amount of code. It is really a type-safe way to define +and configure function to be recognized as Azure Function. +Everything else should be delegated to the base `FunctionInvoker` via `handleRequest(..)` callback which will invoke your function, taking care of +necessary type conversion, transformation etc. One exception to this rule is when custom result handling is required. In that case, the proper post-process method can be overridden as well in order to take control of the results processing. + +`UppercaseHandler.java`: + +```java +public class UppercaseHandler extends FunctionInvoker, Void> { + + @FunctionName("uppercase") + public void execute(@TimerTrigger(name = "keepAliveTrigger", schedule = "0 */1 * * * *") String timerInfo, + ExecutionContext context) { + + Message message = MessageBuilder.withPayload(timerInfo).build(); + + handleRequest(message, context); + } +} +``` + +Note that this function does not return value (e.g. Void output type) and is backed by `java.util.Consumer` SCF implementation: + +```java + @Bean + public Consumer> uppercase() { + return message -> { + // /timeInfo is a JSON string, you can deserialize it to an object using your favorite JSON library + String timeInfo = message.getPayload(); + + // Business logic -> convert the timeInfo to uppercase. + String value = timeInfo.toUpperCase(); + + // (Optionally) access and use the Azure function context. + ExecutionContext context = (ExecutionContext) message.getHeaders().get("executionContext"); + context.getLogger().info("Timer is triggered with TimeInfo: " + value); + + // No response. + }; + } +``` + +## Notes + +* Disable the `spring-boot-maven-plugin` in favor of the `azure-functions-maven-plugin`. +* Exclude the `org.springframework.boot:spring-boot-starter-logging` dependency from the `org.springframework.cloud:spring-cloud-function-adapter-azure`. +* Add `"AzureWebJobsStorage": "UseDevelopmentStorage=true"` to the `local.settings.json`. diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/mvnw b/spring-cloud-function-samples/function-sample-azure-timer-trigger/mvnw new file mode 100755 index 000000000..8a8fb2282 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/mvnw @@ -0,0 +1,316 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/mvnw.cmd b/spring-cloud-function-samples/function-sample-azure-timer-trigger/mvnw.cmd new file mode 100644 index 000000000..1d8ab018e --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/mvnw.cmd @@ -0,0 +1,188 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/pom.xml b/spring-cloud-function-samples/function-sample-azure-timer-trigger/pom.xml new file mode 100644 index 000000000..c891357a7 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/pom.xml @@ -0,0 +1,227 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.15 + + + + example.scf.azure + timer-trigger-azure-spring-function + 0.0.1-SNAPSHOT + timer-trigger-demo + Demo project for Spring Boot + + 1.8 + 2021.0.10-SNAPSHOT + 2.1.0 + + example.TimerTriggerDemoApplication + + example-spring-function-resource-group + timer-trigger-azure-spring-function + westeurope + ${project.build.directory}/azure-functions/${functionAppName} + java-functions-app-service-plan + + + + + org.springframework.cloud + spring-cloud-function-context + + + spring-cloud-function-adapter-azure + org.springframework.cloud + + + org.springframework.boot + spring-boot-starter-logging + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + org.springframework.cloud + spring-cloud-function-dependencies + ${spring-cloud.version} + pom + import + + + com.microsoft.azure.functions + azure-functions-java-library + ${azure.functions.java.core.version} + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-dependency-plugin + + + copy-dependencies + prepare-package + + copy-dependencies + + + ${stagingDirectory}/lib + false + false + true + runtime + azure-functions-java-library + + + + + + com.microsoft.azure + azure-functions-maven-plugin + + + ${functionResourceGroup} + ${functionAppName} + ${functionAppRegion} + ${functionAppServicePlanName} + + linux + 17 + + + + + FUNCTIONS_EXTENSION_VERSION + ~4 + + + FUNCTIONS_WORKER_RUNTIME + java + + + + + + package-functions + + package + + + + + + maven-resources-plugin + + + copy-resources + package + + copy-resources + + + true + + ${stagingDirectory} + + + + ${project.basedir}/src/main/azure + + + ** + + + + + + + + + + maven-clean-plugin + 3.1.0 + + + + obj + + + + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/azure/host.json b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/azure/host.json new file mode 100644 index 000000000..a1e0497a7 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/azure/host.json @@ -0,0 +1,4 @@ +{ + "functionTimeout": "00:05:00", + "version": "2.0" +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/azure/local.settings.json b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/azure/local.settings.json new file mode 100644 index 000000000..adce8b884 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/azure/local.settings.json @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "UseDevelopmentStorage=true", + "AzureWebJobsDashboard": "", + "FUNCTIONS_WORKER_RUNTIME": "java" + } +} \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/java/example/TimerTriggerDemoApplication.java b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/java/example/TimerTriggerDemoApplication.java new file mode 100644 index 000000000..0e5273cf1 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/java/example/TimerTriggerDemoApplication.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example; + +import java.util.function.Consumer; + +import com.microsoft.azure.functions.ExecutionContext; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.messaging.Message; + +@SpringBootApplication +public class TimerTriggerDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(TimerTriggerDemoApplication.class, args); + } + + @Bean + public Consumer> uppercase() { + return message -> { + // /timeInfo is a JSON string, you can deserialize it to an object using your favorite JSON library + String timeInfo = message.getPayload(); + + // Business logic -> convert the timeInfo to uppercase. + String value = timeInfo.toUpperCase(); + + // (Optionally) access and use the Azure function context. + ExecutionContext context = (ExecutionContext) message.getHeaders().get("executionContext"); + context.getLogger().info("Timer is triggered with TimeInfo: " + value); + + // No response. + }; + } +} diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/java/example/UppercaseHandler.java b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/java/example/UppercaseHandler.java new file mode 100644 index 000000000..3c8bb7246 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/java/example/UppercaseHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.TimerTrigger; + +import org.springframework.cloud.function.adapter.azure.FunctionInvoker; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; + +public class UppercaseHandler extends FunctionInvoker, Void> { + + @FunctionName("uppercase") + public void execute(@TimerTrigger(name = "keepAliveTrigger", schedule = "0 */1 * * * *") String timerInfo, + ExecutionContext context) { + + Message message = MessageBuilder.withPayload(timerInfo).build(); + + handleRequest(message, context); + } +} diff --git a/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/resources/application.properties b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/resources/application.properties new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure-timer-trigger/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/spring-cloud-function-samples/function-sample-azure/README.adoc b/spring-cloud-function-samples/function-sample-azure/README.adoc index f20f7d7e3..144d16464 100644 --- a/spring-cloud-function-samples/function-sample-azure/README.adoc +++ b/spring-cloud-function-samples/function-sample-azure/README.adoc @@ -8,7 +8,7 @@ $ mvn azure-functions:run ---- The `uppercase` function is of the following signature `Function, String> uppercase()`. Its expected input is JSON, -therefore we need t0 provide the appropriate content-type (in this case `application/json`). +therefore we need to provide the appropriate content-type (in this case `application/json`). Test the function using _curl_ and notice that the URL is formed by concatenating `/api/` ---- @@ -23,12 +23,12 @@ $ curl -H "Content-Type: application/json" localhost:7071/api/uppercase -d '{"gr ---- The HTTP headers of the incoming request will be copied into input Message's MessageHeaders, so they become accessible if need to. -It is done in implementation of `UppercaseHandler` which extends `FunctionInvoker`. +It is done in implementation of `UppercaseHandler` which extends `FunctionInvoker`. NOTE: Implementation of `FunctionInvoker` (your handler), should contain the least amount of code. It is really a type-safe way to define and configure function to be recognized as Azure Function. Everything else should be delegated to the base `FunctionInvoker` via `handleRequest(..)` callback which will invoke your function, taking care of -necessary type conversion, transformation etc. +necessary type conversion, transformation etc. One exception to this rule is when custom result handling is required. In that case, the proper post-process method can be overridden as well in order to take control of the results processing. ---- @FunctionName("uppercase") @@ -42,7 +42,7 @@ public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, The `echo` function does the same as the `uppercase` less the actual uppercasing. However, the important difference to notice is that function itself -takes primitive `String` as its input (i.e., `public Function echo()`) while the actual handler passes instance of `Message` the same way as with `uppercase`. The framework recognizes that you only care about the payload and extracts it from the `Message` before calling the function. +takes primitive `String` as its input (i.e., `public Function echo()`) while the actual handler passes instance of `Message` the same way as with `uppercase`. The framework recognizes that you only care about the payload and extracts it from the `Message` before calling the function. There is also a reactive version of 'uppercase' - `uppercaseReactive` which will produce the same result, but @@ -74,7 +74,7 @@ $ mvn azure-functions:deploy # the function can be accessed at: https://function-sample-azure.azurewebsites.net/api/uppercase ---- -On another terminal try this: +On another terminal try this: ---- # testing curl https:///api/uppercase -d '{"greeting": "hello", "name": "your name"}' @@ -88,7 +88,7 @@ $ curl -H "Content-Type: application/json" https://function-sample-azure.azurewe } ---- -Please ensure that you use the right URL for the function above. +Please ensure that you use the right URL for the function above. Alternatively you can test the function in the Azure Dashboard UI: @@ -111,3 +111,27 @@ Alternatively you can test the function in the Azure Dashboard UI: ---- Please note that the Dashhboard provides by default information on Function Execution Count, Memory Consumption and Execution Time. + +==== Custom Result Handling + +As noted above, the implementation of `FunctionInvoker` (your handler), should contain the least amount of code possible. However, if custom result handling needs to occur there is a set of methods (named `postProcess**`) that can be overridden in link:../../spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java[FunctionInvoker.java]. + +One such example can be seen in link:src/main/java/example/ReactiveEchoCustomResultHandler.java[ReactiveEchoCustomResultHandler.java]. + +Once the function is deployed it can be tested using _curl_ +---- +$ curl -H "Content-Type: application/json" localhost:7071/api/echoStream -d '["hello","peepz"]' + +# result +Kicked off job for [hello, peepz] +---- +The custom result handling takes the Flux returned from the `echoStream` function and adds logging, uppercase mapping, and then subscribes to the publisher. The Azure logs output the following: + +---- +[2022-03-01T01:36:57.439Z] 2022-02-28 19:36:57.439 INFO 20587 --- [pool-2-thread-2] o.s.boot.SpringApplication : Started application in 0.466 seconds (JVM running for 57.906) +[2022-03-01T01:36:57.462Z] BEGIN echo post-processing work ... +[2022-03-01T01:36:57.462Z] HELLO +[2022-03-01T01:36:57.462Z] PEEPZ +[2022-03-01T01:36:57.463Z] END echo post-processing work +[2022-03-01T01:36:57.463Z] Function "echoStream" (Id: 678cff0b-d958-4fab-967b-e19e0d5d67e8) invoked by Java Worker +---- diff --git a/spring-cloud-function-samples/function-sample-azure/pom.xml b/spring-cloud-function-samples/function-sample-azure/pom.xml index fa3844956..55b65f9ce 100644 --- a/spring-cloud-function-samples/function-sample-azure/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure/pom.xml @@ -14,7 +14,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -34,6 +34,7 @@ org.springframework.cloud spring-cloud-function-adapter-azure + 3.2.9-SNAPSHOT org.springframework.cloud @@ -54,7 +55,7 @@ org.springframework.cloud spring-cloud-function-dependencies - 3.2.1-SNAPSHOT + 3.2.3-SNAPSHOT pom import diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java index 8bfd1c78c..b40249436 100644 --- a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,16 +19,20 @@ import java.util.Map; import java.util.function.Function; +import com.microsoft.azure.functions.ExecutionContext; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.annotation.Bean; import org.springframework.messaging.Message; -import com.microsoft.azure.functions.ExecutionContext; - -import reactor.core.publisher.Mono; - +/** + * @author Oleg Zhurakousky + * @author Chris Bono + */ @SpringBootApplication public class Config { @@ -67,13 +71,14 @@ public Function, String> uppercase(JsonMapper mapper) { }; } - @Bean public Function, Mono> uppercaseReactive() { - return mono -> mono.map(value -> { - return value.toUpperCase(); - }); + return mono -> mono.map(value -> value.toUpperCase()); } + @Bean + public Function, Flux> echoStream() { + return flux -> flux.map(value -> value.toUpperCase()); + } } diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveEchoCustomResultHandler.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveEchoCustomResultHandler.java new file mode 100644 index 000000000..8f72a80bc --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveEchoCustomResultHandler.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example; + +import java.util.List; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Flux; + +import org.springframework.cloud.function.adapter.azure.FunctionInvoker; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; + +/** + * Sample that shows how to customize the default function result handling by operating on the {@link Flux} returned + * from the {@link Config#echoStream()} echoStream} function. + * + * @author Chris Bono + */ +public class ReactiveEchoCustomResultHandler extends FunctionInvoker, String> { + + private static final Log logger = LogFactory.getLog(ReactiveEchoCustomResultHandler.class); + + @FunctionName("echoStream") + public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) + HttpRequestMessage> request, ExecutionContext context + ) { + return handleRequest(request.getBody(), context); + } + + @Override + protected String postProcessFluxFunctionResult(List rawInputs, Object functionInputs, Flux functionResult, + FunctionInvocationWrapper function, ExecutionContext executionContext + ) { + functionResult + .doFirst(() -> executionContext.getLogger().info("BEGIN echo post-processing work ...")) + .mapNotNull((v) -> v.toString().toUpperCase()) + .doFinally((signalType) -> executionContext.getLogger().info("END echo post-processing work")) + .subscribe((v) -> executionContext.getLogger().info(" " + v)); + return "Kicked off job for " + rawInputs; + } + +} diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/pom.xml b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/pom.xml index e81cb2cfd..26e1dbdc0 100644 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/pom.xml +++ b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/pom.xml @@ -12,13 +12,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-samples/function-sample-cloudevent-stream/pom.xml b/spring-cloud-function-samples/function-sample-cloudevent-stream/pom.xml index 3d6289177..53934ea26 100644 --- a/spring-cloud-function-samples/function-sample-cloudevent-stream/pom.xml +++ b/spring-cloud-function-samples/function-sample-cloudevent-stream/pom.xml @@ -11,13 +11,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-samples/function-sample-cloudevent/pom.xml b/spring-cloud-function-samples/function-sample-cloudevent/pom.xml index 8e9675a83..3fbb38d8c 100644 --- a/spring-cloud-function-samples/function-sample-cloudevent/pom.xml +++ b/spring-cloud-function-samples/function-sample-cloudevent/pom.xml @@ -11,13 +11,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-samples/function-sample-compiler/.jdk8 b/spring-cloud-function-samples/function-sample-compiler/.jdk8 deleted file mode 100644 index e69de29bb..000000000 diff --git a/spring-cloud-function-samples/function-sample-compiler/build.gradle b/spring-cloud-function-samples/function-sample-compiler/build.gradle deleted file mode 100644 index ee3becec8..000000000 --- a/spring-cloud-function-samples/function-sample-compiler/build.gradle +++ /dev/null @@ -1,51 +0,0 @@ -buildscript { - ext { - springBootVersion = '1.5.12.RELEASE' - wrapperVersion = '1.0.11.RELEASE' - } - repositories { - mavenLocal() - mavenCentral() - maven { url "https://repo.spring.io/plugins-snapshot" } - maven { url "https://repo.spring.io/plugins-milestone" } - } - dependencies { - classpath("org.springframework.boot.experimental:spring-boot-thin-gradle-plugin:${wrapperVersion}") - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") - } -} - -apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'eclipse' -apply plugin: 'spring-boot' -apply plugin: 'org.springframework.boot.experimental.thin-launcher' - -group = 'io.spring.sample' -version = '2.0.0.RELEASE' -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -repositories { - mavenLocal() - mavenCentral() - maven { url "https://repo.spring.io/snapshot" } - maven { url "https://repo.spring.io/milestone" } -} - -ext { - springCloudFunctionVersion = "2.0.0.BUILD-SNAPSHOT" -} -ext['reactor.version'] = "3.1.7.RELEASE" - -dependencyManagement { - imports { - mavenBom "org.springframework.cloud:spring-cloud-function-dependencies:${springCloudFunctionVersion}" - } -} - -dependencies { - compile('org.springframework.cloud:spring-cloud-starter-function-web') - compile('org.springframework.cloud:spring-cloud-function-compiler') - testCompile('org.springframework.boot:spring-boot-starter-test') -} diff --git a/spring-cloud-function-samples/function-sample-compiler/gradle/wrapper/gradle-wrapper.jar b/spring-cloud-function-samples/function-sample-compiler/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index ca78035ef..000000000 Binary files a/spring-cloud-function-samples/function-sample-compiler/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/spring-cloud-function-samples/function-sample-compiler/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-function-samples/function-sample-compiler/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index a0b285197..000000000 --- a/spring-cloud-function-samples/function-sample-compiler/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.4.1-bin.zip diff --git a/spring-cloud-function-samples/function-sample-compiler/gradlew b/spring-cloud-function-samples/function-sample-compiler/gradlew deleted file mode 100755 index 27309d923..000000000 --- a/spring-cloud-function-samples/function-sample-compiler/gradlew +++ /dev/null @@ -1,164 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-cloud-function-samples/function-sample-compiler/gradlew.bat b/spring-cloud-function-samples/function-sample-compiler/gradlew.bat deleted file mode 100644 index f6d5974e7..000000000 --- a/spring-cloud-function-samples/function-sample-compiler/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/spring-cloud-function-samples/function-sample-compiler/pom.xml b/spring-cloud-function-samples/function-sample-compiler/pom.xml index b8958a7b2..de03d519b 100644 --- a/spring-cloud-function-samples/function-sample-compiler/pom.xml +++ b/spring-cloud-function-samples/function-sample-compiler/pom.xml @@ -14,13 +14,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 3.1.2.RELEASE 1.0.17.RELEASE diff --git a/spring-cloud-function-samples/function-sample-compiler/src/test/java/com/example/SampleCompiledConsumerTests.java b/spring-cloud-function-samples/function-sample-compiler/src/test/java/com/example/SampleCompiledConsumerTests.java deleted file mode 100644 index 7b007aa79..000000000 --- a/spring-cloud-function-samples/function-sample-compiler/src/test/java/com/example/SampleCompiledConsumerTests.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.web.server.LocalServerPort; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Mark Fisher - */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { - "spring.cloud.function.compile.test.lambda=com.example.SampleCompiledConsumerTests.Reference::set", - "spring.cloud.function.compile.test.inputType=String", - "spring.cloud.function.compile.test.type=consumer"}) -public class SampleCompiledConsumerTests { - - @LocalServerPort - private int port; - - @Test - public void print() { - assertThat(new TestRestTemplate().postForObject( - "http://localhost:" + this.port + "/test", "it works", String.class)) - .isNull(); - assertThat(Reference.instance).isEqualTo("it works"); - } - - public static class Reference { - - private static Object instance; - - public static void set(Object o) { - instance = o; - } - - } - -} diff --git a/spring-cloud-function-samples/function-sample-compiler/src/test/java/com/example/SampleCompiledFunctionTests.java b/spring-cloud-function-samples/function-sample-compiler/src/test/java/com/example/SampleCompiledFunctionTests.java deleted file mode 100644 index 9f702be13..000000000 --- a/spring-cloud-function-samples/function-sample-compiler/src/test/java/com/example/SampleCompiledFunctionTests.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2012-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.example; - -import org.junit.jupiter.api.Test; - -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.boot.web.server.LocalServerPort; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Mark Fisher - */ -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { - "spring.cloud.function.compile.test.lambda=f->f.map(s->s+\"!!!\")", - "spring.cloud.function.compile.test.inputType=Flux", - "spring.cloud.function.compile.test.outputType=Flux"}) -public class SampleCompiledFunctionTests { - - @LocalServerPort - private int port; - - @Test - public void lowercase() { - assertThat(new TestRestTemplate().postForObject( - "http://localhost:" + this.port + "/test", "it works", String.class)) - .contains("it works!!!"); - } - -} diff --git a/spring-cloud-function-samples/function-sample-functional-aws-routing/pom.xml b/spring-cloud-function-samples/function-sample-functional-aws-routing/pom.xml index a148ca0f0..f9f9d5eb5 100644 --- a/spring-cloud-function-samples/function-sample-functional-aws-routing/pom.xml +++ b/spring-cloud-function-samples/function-sample-functional-aws-routing/pom.xml @@ -15,7 +15,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -25,7 +25,7 @@ 1.8 1.0.27.RELEASE 2.0.2 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-functional-aws-routing/src/main/java/example/FunctionConfiguration.java b/spring-cloud-function-samples/function-sample-functional-aws-routing/src/main/java/example/FunctionConfiguration.java index 9aa1eea0c..554133da0 100644 --- a/spring-cloud-function-samples/function-sample-functional-aws-routing/src/main/java/example/FunctionConfiguration.java +++ b/spring-cloud-function-samples/function-sample-functional-aws-routing/src/main/java/example/FunctionConfiguration.java @@ -5,12 +5,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.FunctionType; import org.springframework.cloud.function.context.MessageRoutingCallback; -import org.springframework.cloud.function.context.MessageRoutingCallback.FunctionRoutingResult; -import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.annotation.Bean; import org.springframework.context.support.GenericApplicationContext; import org.springframework.messaging.Message; @@ -51,9 +48,9 @@ public void initialize(GenericApplicationContext applicationContext) { () -> new RoutingCallback()); applicationContext.registerBean("uppercase", FunctionRegistration.class, () -> new FunctionRegistration<>(uppercase()).type( - FunctionType.from(String.class).to(String.class))); + FunctionTypeUtils.functionType(String.class, String.class))); applicationContext.registerBean("reverse", FunctionRegistration.class, () -> new FunctionRegistration<>(reverse()).type( - FunctionType.from(String.class).to(String.class))); + FunctionTypeUtils.functionType(String.class, String.class))); } } diff --git a/spring-cloud-function-samples/function-sample-gcp-background/pom.xml b/spring-cloud-function-samples/function-sample-gcp-background/pom.xml index 29568d01b..3e20e59ab 100644 --- a/spring-cloud-function-samples/function-sample-gcp-background/pom.xml +++ b/spring-cloud-function-samples/function-sample-gcp-background/pom.xml @@ -15,7 +15,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 diff --git a/spring-cloud-function-samples/function-sample-gcp-http/pom.xml b/spring-cloud-function-samples/function-sample-gcp-http/pom.xml index 01a5e6403..60b52c0fd 100644 --- a/spring-cloud-function-samples/function-sample-gcp-http/pom.xml +++ b/spring-cloud-function-samples/function-sample-gcp-http/pom.xml @@ -15,12 +15,12 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 - 3.2.1-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-grpc-cloudevent/pom.xml b/spring-cloud-function-samples/function-sample-grpc-cloudevent/pom.xml index eade0e637..4db3ea4fd 100644 --- a/spring-cloud-function-samples/function-sample-grpc-cloudevent/pom.xml +++ b/spring-cloud-function-samples/function-sample-grpc-cloudevent/pom.xml @@ -7,7 +7,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 com.example.grpc @@ -17,7 +17,7 @@ Demo project for Spring Boot 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT @@ -86,7 +86,7 @@ 0.6.1 - com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier} + com.google.protobuf:protoc:4.32.0:exe:${os.detected.classifier} grpc-java diff --git a/spring-cloud-function-samples/function-sample-kotlin-web/pom.xml b/spring-cloud-function-samples/function-sample-kotlin-web/pom.xml index b80bfa41f..b84dd6810 100644 --- a/spring-cloud-function-samples/function-sample-kotlin-web/pom.xml +++ b/spring-cloud-function-samples/function-sample-kotlin-web/pom.xml @@ -11,7 +11,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -38,17 +38,17 @@ org.springframework.cloud spring-cloud-function-kotlin - 3.2.1-SNAPSHOT + 3.2.13-SNAPSHOT org.springframework.cloud spring-cloud-function-web - 3.2.1-SNAPSHOT + 3.2.13-SNAPSHOT org.springframework.cloud spring-cloud-function-context - 3.2.1-SNAPSHOT + 3.2.13-SNAPSHOT org.springframework.boot diff --git a/spring-cloud-function-samples/function-sample-pof/build.gradle b/spring-cloud-function-samples/function-sample-pof/build.gradle index ee3becec8..d62d81852 100644 --- a/spring-cloud-function-samples/function-sample-pof/build.gradle +++ b/spring-cloud-function-samples/function-sample-pof/build.gradle @@ -46,6 +46,5 @@ dependencyManagement { dependencies { compile('org.springframework.cloud:spring-cloud-starter-function-web') - compile('org.springframework.cloud:spring-cloud-function-compiler') testCompile('org.springframework.boot:spring-boot-starter-test') } diff --git a/spring-cloud-function-samples/function-sample-pof/pom.xml b/spring-cloud-function-samples/function-sample-pof/pom.xml index 45b53f715..a14bbb0ad 100644 --- a/spring-cloud-function-samples/function-sample-pof/pom.xml +++ b/spring-cloud-function-samples/function-sample-pof/pom.xml @@ -13,7 +13,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -22,7 +22,7 @@ UTF-8 1.8 3.1.2.RELEASE - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-pojo/build.gradle b/spring-cloud-function-samples/function-sample-pojo/build.gradle index 47409b0e9..d033c4a27 100644 --- a/spring-cloud-function-samples/function-sample-pojo/build.gradle +++ b/spring-cloud-function-samples/function-sample-pojo/build.gradle @@ -46,6 +46,5 @@ dependencyManagement { dependencies { compile('org.springframework.cloud:spring-cloud-starter-function-web') - compile('org.springframework.cloud:spring-cloud-function-compiler') testCompile('org.springframework.boot:spring-boot-starter-test') } diff --git a/spring-cloud-function-samples/function-sample-pojo/pom.xml b/spring-cloud-function-samples/function-sample-pojo/pom.xml index fd25c5a77..d37702656 100644 --- a/spring-cloud-function-samples/function-sample-pojo/pom.xml +++ b/spring-cloud-function-samples/function-sample-pojo/pom.xml @@ -14,13 +14,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE diff --git a/spring-cloud-function-samples/function-sample-spring-integration/pom.xml b/spring-cloud-function-samples/function-sample-spring-integration/pom.xml index 6c0ea20e9..b1cd5e636 100644 --- a/spring-cloud-function-samples/function-sample-spring-integration/pom.xml +++ b/spring-cloud-function-samples/function-sample-spring-integration/pom.xml @@ -12,7 +12,7 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 @@ -20,7 +20,7 @@ UTF-8 UTF-8 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml b/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml index 38aafb06d..1cb3bf56c 100644 --- a/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml @@ -14,13 +14,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-task/build.gradle b/spring-cloud-function-samples/function-sample-task/build.gradle index 639b41f75..91518d022 100644 --- a/spring-cloud-function-samples/function-sample-task/build.gradle +++ b/spring-cloud-function-samples/function-sample-task/build.gradle @@ -46,6 +46,5 @@ dependencyManagement { dependencies { compile('org.springframework.cloud:spring-cloud-function-task') - compile('org.springframework.cloud:spring-cloud-function-compiler') testCompile('org.springframework.boot:spring-boot-starter-test') } diff --git a/spring-cloud-function-samples/function-sample-task/pom.xml b/spring-cloud-function-samples/function-sample-task/pom.xml index bfa512c92..63a4ad719 100644 --- a/spring-cloud-function-samples/function-sample-task/pom.xml +++ b/spring-cloud-function-samples/function-sample-task/pom.xml @@ -14,13 +14,13 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.10.RELEASE 3.1.2.RELEASE @@ -34,10 +34,6 @@ org.springframework.cloud spring-cloud-starter-function-web - - org.springframework.cloud - spring-cloud-function-compiler - org.springframework.boot spring-boot-starter-test diff --git a/spring-cloud-function-samples/function-sample/build.gradle b/spring-cloud-function-samples/function-sample/build.gradle index 76eb4efff..f2976cab9 100644 --- a/spring-cloud-function-samples/function-sample/build.gradle +++ b/spring-cloud-function-samples/function-sample/build.gradle @@ -45,6 +45,5 @@ dependencyManagement { dependencies { compile('org.springframework.cloud:spring-cloud-starter-function-web') - compile('org.springframework.cloud:spring-cloud-function-compiler') testCompile('org.springframework.boot:spring-boot-starter-test') } diff --git a/spring-cloud-function-samples/function-sample/pom.xml b/spring-cloud-function-samples/function-sample/pom.xml index 8a89e86c0..4d20ed162 100644 --- a/spring-cloud-function-samples/function-sample/pom.xml +++ b/spring-cloud-function-samples/function-sample/pom.xml @@ -1,7 +1,7 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 io.spring.sample @@ -14,21 +14,21 @@ org.springframework.boot spring-boot-starter-parent - 2.6.1 + 2.6.15 1.8 - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT 1.0.27.RELEASE - org.springframework.boot - spring-boot-starter-actuator - + org.springframework.boot + spring-boot-starter-actuator + org.springframework.cloud spring-cloud-starter-function-webflux diff --git a/spring-cloud-function-samples/function-sample-compiler/src/main/java/com/example/SampleApplication.java b/spring-cloud-function-samples/function-sample/src/main/java/com/example/Client.java similarity index 58% rename from spring-cloud-function-samples/function-sample-compiler/src/main/java/com/example/SampleApplication.java rename to spring-cloud-function-samples/function-sample/src/main/java/com/example/Client.java index d66601624..516fa3a1c 100644 --- a/spring-cloud-function-samples/function-sample-compiler/src/main/java/com/example/SampleApplication.java +++ b/spring-cloud-function-samples/function-sample/src/main/java/com/example/Client.java @@ -16,16 +16,29 @@ package com.example; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.web.reactive.function.client.WebClient; -// @checkstyle:off -@SpringBootApplication -public class SampleApplication { +/** + * Sample client to test infinite stream from function. + * + * @author Oleg Zhurakousky + * + */ +public class Client { public static void main(String[] args) throws Exception { - SpringApplication.run(SampleApplication.class, args); + WebClient client = WebClient.create(); + WebClient.ResponseSpec responseSpec = client.post() + .uri("http://localhost:8080/infinite") + .header("accept", "text/event-stream") + .retrieve(); + + responseSpec.bodyToFlux(String.class).subscribe(v -> { + System.out.println(v); + }); + + System.in.read(); + } } -// @checkstyle:on diff --git a/spring-cloud-function-samples/function-sample/src/main/java/com/example/SampleApplication.java b/spring-cloud-function-samples/function-sample/src/main/java/com/example/SampleApplication.java index 4fb75cf8c..f26aec07f 100644 --- a/spring-cloud-function-samples/function-sample/src/main/java/com/example/SampleApplication.java +++ b/spring-cloud-function-samples/function-sample/src/main/java/com/example/SampleApplication.java @@ -16,10 +16,12 @@ package com.example; +import java.time.Duration; import java.util.function.Function; import java.util.function.Supplier; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -55,9 +57,11 @@ public Supplier hello() { } @Bean - public Supplier> words() { - return () -> Flux.fromArray(new String[] {"foo", "bar"}); + public Supplier> infinite() { + return () -> Flux + .interval(Duration.ofSeconds(1)) + .log() + .map(counter -> String.format("Counter: %s", counter)); } } -// @checkstyle:on diff --git a/spring-cloud-function-samples/function-sample/src/test/java/com/example/FunctionTests.java b/spring-cloud-function-samples/function-sample/src/test/java/com/example/FunctionTests.java index 9248f1030..cae8e9187 100644 --- a/spring-cloud-function-samples/function-sample/src/test/java/com/example/FunctionTests.java +++ b/spring-cloud-function-samples/function-sample/src/test/java/com/example/FunctionTests.java @@ -51,15 +51,6 @@ public void testHello() { assertThat(output).isEqualTo("hello"); } - @Test - public void testWords() { - Flux output = this.functions.words().get(); - List results = output.collectList().block(); - assertThat(results.size()).isEqualTo(2); - assertThat(results.get(0)).isEqualTo("foo"); - assertThat(results.get(1)).isEqualTo("bar"); - } - @Test public void testGreeter() { assertThat(new Greeter().apply("World")).isEqualTo("Hello World"); diff --git a/spring-cloud-function-samples/function-sample/src/test/java/com/example/WebTestClientTests.java b/spring-cloud-function-samples/function-sample/src/test/java/com/example/WebTestClientTests.java index c74216e7b..9e4cfc703 100644 --- a/spring-cloud-function-samples/function-sample/src/test/java/com/example/WebTestClientTests.java +++ b/spring-cloud-function-samples/function-sample/src/test/java/com/example/WebTestClientTests.java @@ -19,12 +19,6 @@ public class WebTestClientTests { @Autowired private WebTestClient client; - @Test - public void words() { - client.get().uri("/words").exchange() - .expectStatus().isOk().expectBody(String.class).isEqualTo("[\"foo\",\"bar\"]"); - } - @Test public void uppercase() { client.post().uri("/uppercase").body(Mono.just("foo"), String.class).exchange() diff --git a/spring-cloud-function-samples/pom.xml b/spring-cloud-function-samples/pom.xml index 69fdcc2c5..93a50a0a6 100644 --- a/spring-cloud-function-samples/pom.xml +++ b/spring-cloud-function-samples/pom.xml @@ -11,10 +11,13 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT + function-sample-pof function-sample-pojo function-sample-aws diff --git a/spring-cloud-function-task/pom.xml b/spring-cloud-function-task/pom.xml index fef2cfda4..e426b5733 100644 --- a/spring-cloud-function-task/pom.xml +++ b/spring-cloud-function-task/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-web/pom.xml b/spring-cloud-function-web/pom.xml index 6ae02e9f2..01fb55aeb 100644 --- a/spring-cloud-function-web/pom.xml +++ b/spring-cloud-function-web/pom.xml @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java index b04c95212..97b710ef7 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionController.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.web.flux; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -74,27 +75,25 @@ public Mono> post(ServerWebExchange request, return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, false); } - @SuppressWarnings("unchecked") @PostMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @ResponseBody - public Mono> postStream(ServerWebExchange request, @RequestBody(required = false) Flux body) { - return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, false); + public Publisher postStream(ServerWebExchange request, @RequestBody(required = false) Flux body) { + return FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, true); } - @SuppressWarnings("unchecked") - @GetMapping(path = "/**") + @GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @ResponseBody - public Mono> get(ServerWebExchange request) { + public Publisher getStream(ServerWebExchange request) { FunctionWrapper wrapper = wrapper(request); - return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false); + return FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true); } @SuppressWarnings("unchecked") - @GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + @GetMapping(path = "/**") @ResponseBody - public Mono> getStream(ServerWebExchange request) { + public Mono> get(ServerWebExchange request) { FunctionWrapper wrapper = wrapper(request); - return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), true); + return (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, wrapper.getArgument(), false); } private FunctionWrapper wrapper(ServerWebExchange request) { diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java index 0d08f3e91..f8de9c1ca 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionController.java @@ -94,21 +94,18 @@ public Mono>> postStream(WebRequest request, .headers(response.getHeaders()).body((Publisher) response.getBody())); } - @SuppressWarnings("unchecked") @GetMapping(path = "/**", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @ResponseBody - public Mono>> getStream(WebRequest request) { + public Publisher getStream(WebRequest request) { FunctionWrapper wrapper = wrapper(request); - return ((Mono>) FunctionWebRequestProcessingHelper - .processRequest(wrapper, wrapper.getArgument(), true)).map(response -> ResponseEntity.ok() - .headers(response.getHeaders()).body((Publisher) response.getBody())); + return FunctionWebRequestProcessingHelper + .processRequest(wrapper, wrapper.getArgument(), true); } @PostMapping(path = "/**") @ResponseBody public Object post(WebRequest request, @RequestBody(required = false) String body) { - String argument = StringUtils.hasText(body) ? body : ""; - return FunctionWebRequestProcessingHelper.processRequest(wrapper(request), argument, false); + return FunctionWebRequestProcessingHelper.processRequest(wrapper(request), body, false); } @GetMapping(path = "/**") diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java index b459a6954..3f876133f 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java @@ -84,20 +84,28 @@ public static Object invokeFunction(FunctionInvocationWrapper function, Object i } @SuppressWarnings({ "rawtypes", "unchecked" }) - public static Object processRequest(FunctionWrapper wrapper, Object argument, boolean eventStream) { + public static Publisher processRequest(FunctionWrapper wrapper, Object argument, boolean eventStream) { + if (argument == null) { + argument = ""; + } FunctionInvocationWrapper function = wrapper.getFunction(); HttpHeaders headers = wrapper.getHeaders(); - Message inputMessage = argument == null ? null : MessageBuilder.withPayload(argument).copyHeaders(headers.toSingleValueMap()).build(); + Message inputMessage = null; + + + MessageBuilder builder = MessageBuilder.withPayload(argument); + if (!CollectionUtils.isEmpty(wrapper.getParams())) { + builder = builder.setHeader(HeaderUtils.HTTP_REQUEST_PARAM, wrapper.getParams().toSingleValueMap()); + } + inputMessage = builder.copyHeaders(headers.toSingleValueMap()).build(); if (function.isRoutingFunction()) { function.setSkipOutputConversion(true); } - Object input = argument == null ? Flux.empty() : (argument instanceof Publisher ? Flux.from((Publisher) argument) : inputMessage); - - Object result = function.apply(input); + Object result = function.apply(inputMessage); if (function.isConsumer()) { if (result instanceof Publisher) { Mono.from((Publisher) result).subscribe(); @@ -111,7 +119,7 @@ public static Object processRequest(FunctionWrapper wrapper, Object argument, bo if (result instanceof Publisher) { pResult = (Publisher) result; if (eventStream) { - return Flux.from(pResult).then(Mono.fromSupplier(() -> responseOkBuilder.body(result))); + return Flux.from(pResult); } if (pResult instanceof Flux) { diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/HeaderUtils.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/HeaderUtils.java index 50e445dbe..f16c1afb2 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/HeaderUtils.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/HeaderUtils.java @@ -31,6 +31,11 @@ */ public final class HeaderUtils { + /** + * Message Header name which contains HTTP request parameters. + */ + public static final String HTTP_REQUEST_PARAM = "http_request_param"; + private static HttpHeaders IGNORED = new HttpHeaders(); private static HttpHeaders REQUEST_ONLY = new HttpHeaders(); diff --git a/spring-cloud-function-web/src/main/resources/META-INF/spring.factories b/spring-cloud-function-web/src/main/resources/META-INF/spring.factories index 62f7b592b..8657bf5ac 100644 --- a/spring-cloud-function-web/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-function-web/src/main/resources/META-INF/spring.factories @@ -3,8 +3,7 @@ org.springframework.cloud.function.web.flux.ReactorAutoConfiguration,\ org.springframework.cloud.function.web.mvc.ReactorAutoConfiguration,\ org.springframework.cloud.function.web.source.FunctionExporterAutoConfiguration org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc=\ -org.springframework.cloud.function.web.flux.ReactorAutoConfiguration,\ -org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration +org.springframework.cloud.function.web.flux.ReactorAutoConfiguration org.springframework.context.ApplicationContextInitializer=\ org.springframework.cloud.function.web.function.FunctionEndpointInitializer,\ org.springframework.cloud.function.web.source.FunctionExporterInitializer diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/flux/FluxRestApplicationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/flux/FluxRestApplicationTests.java index e337cce82..466846d5b 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/flux/FluxRestApplicationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/flux/FluxRestApplicationTests.java @@ -168,7 +168,7 @@ public void timeout() throws Exception { .getBody()).isEqualTo("[\"foo\"]"); } - @Test + //@Test public void emptyJson() throws Exception { assertThat(this.rest .exchange(RequestEntity.get(new URI("/empty")) diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalExporterTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalExporterTests.java index a75d0806d..79ff42e2f 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalExporterTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalExporterTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -53,6 +54,7 @@ "spring.cloud.function.web.export.sink.name=origin|uppercase", "spring.cloud.function.web.export.sink.contentType=text/plain", "spring.cloud.function.web.export.debug=true" }) +@Disabled public class FunctionalExporterTests { @Autowired diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpPostIntegrationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpPostIntegrationTests.java index 1cc7b8a0a..37d1680ad 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpPostIntegrationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpPostIntegrationTests.java @@ -333,6 +333,8 @@ public void uppercaseJsonArray() throws Exception { @Test @DirtiesContext public void uppercaseSSE() throws Exception { + String s = this.rest.exchange(RequestEntity.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON) + .body("[\"foo\",\"bar\"]"), String.class).getBody(); assertThat(this.rest.exchange(RequestEntity.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON) .body("[\"foo\",\"bar\"]"), String.class).getBody()) .isEqualTo(sse("(FOO)", "(BAR)")); diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java index 721ae6e23..6e3d27774 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerTests.java @@ -16,7 +16,11 @@ package org.springframework.cloud.function.web.function; + import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -25,16 +29,24 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionType; import org.springframework.cloud.function.context.FunctionalSpringApplication; import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.util.SocketUtils; +import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -57,6 +69,29 @@ public void close() throws Exception { System.clearProperty("server.port"); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testEmptyBodyRequestParameters() throws Exception { + SpringApplication.run(BeansConfiguration.class); + String port = System.getProperty("server.port"); + TestRestTemplate testRestTemplate = new TestRestTemplate(); + Map params = new HashMap<>(); + params.put("fname", "Jim"); + params.put("lname", "Lahey"); + + HttpHeaders headers = new HttpHeaders(); + headers.set("Accept", "application/json"); + HttpEntity entity = new HttpEntity(headers); + + String urlTemplate = UriComponentsBuilder.fromHttpUrl("http://localhost:" + port + "/nullPayload") + .queryParam("fname", "Jim").queryParam("lname", "Lahey").encode().toUriString(); + + ResponseEntity response = testRestTemplate.exchange(urlTemplate, HttpMethod.GET, entity, String.class); + String res = response.getBody(); + assertThat(res).contains("Jim"); + assertThat(res).contains("Lahey"); + } + @Test public void testNonExistingFunction() throws Exception { FunctionalSpringApplication.run(ApplicationConfiguration.class); @@ -144,6 +179,17 @@ public void initialize(GenericApplicationContext applicationContext) { } + @EnableAutoConfiguration + @Configuration + protected static class BeansConfiguration { + @Bean + public BiFunction, Map> nullPayload() { + return (p, h) -> { + return h; + }; + } + } + @SpringBootConfiguration protected static class ApplicationConfiguration diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/UserSubmittedTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/UserSubmittedTests.java index ac30ea91c..f524ba30f 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/UserSubmittedTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/UserSubmittedTests.java @@ -64,6 +64,18 @@ public void testIssue274() throws Exception { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); } + @Test + public void testIssue274WithData() throws Exception { + SpringApplication.run(Issue274Configuration.class); + TestRestTemplate testRestTemplate = new TestRestTemplate(); + String port = System.getProperty("server.port"); + Thread.sleep(200); + ResponseEntity response = testRestTemplate + .postForEntity(new URI("http://localhost:" + port + "/echo"), "hello", String.class); + assertThat(response.getBody()).isEqualTo("HELLO"); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + } + @SpringBootApplication protected static class Issue274Configuration { diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpPostIntegrationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpPostIntegrationTests.java index 2698bf70f..9115a5fb9 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpPostIntegrationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpPostIntegrationTests.java @@ -290,8 +290,7 @@ public void uppercaseJsonArray() throws Exception { @Test public void uppercaseSSE() throws Exception { - assertThat(this.rest.exchange(RequestEntity.post(new URI("/uppercase")) - .accept(EVENT_STREAM).contentType(MediaType.APPLICATION_JSON) + assertThat(this.rest.exchange(RequestEntity.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON) .body("[\"foo\",\"bar\"]"), String.class).getBody()) .isEqualTo(sse("(FOO)", "(BAR)")); } @@ -334,7 +333,7 @@ public void count() throws Exception { } private String sse(String... values) { - return "data:" + StringUtils.arrayToDelimitedString(values, "\n\ndata:") + "\n\n"; + return "[\"" + StringUtils.arrayToDelimitedString(values, "\",\"") + "\"]"; } @EnableAutoConfiguration diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/PrefixTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/PrefixTests.java index 26f4179c7..d8b264cb9 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/PrefixTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/PrefixTests.java @@ -17,6 +17,8 @@ package org.springframework.cloud.function.web.mvc; import java.net.URI; +import java.util.Map; +import java.util.function.Function; import java.util.function.Supplier; import org.junit.jupiter.api.Test; @@ -29,10 +31,12 @@ import org.springframework.boot.test.web.client.TestRestTemplate; import org.springframework.cloud.function.web.RestApplication; import org.springframework.cloud.function.web.mvc.PrefixTests.TestConfiguration; +import org.springframework.cloud.function.web.util.HeaderUtils; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; +import org.springframework.messaging.Message; import org.springframework.test.context.ContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; @@ -65,6 +69,15 @@ public void missing() throws Exception { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } + + @Test + public void uppercase() throws Exception { + ResponseEntity result = this.rest.exchange( + RequestEntity.get(new URI("/functions/uppercase/foo?nome=Doe&prenome=John")).build(), String.class); + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]"); + } + @EnableAutoConfiguration @org.springframework.boot.test.context.TestConfiguration protected static class TestConfiguration { @@ -74,6 +87,17 @@ public Supplier> words() { return () -> Flux.fromArray(new String[] { "foo", "bar" }); } + @Bean + public Function, String[]> uppercase() { + return message -> { + assertThat(message.getPayload().equals("foo")); + Map httpParam = (Map) message.getHeaders().get(HeaderUtils.HTTP_REQUEST_PARAM); + assertThat(httpParam.get("nome")).isEqualTo("Doe"); + assertThat(httpParam.get("prenome")).isEqualTo("John"); + return new String[] { "foo", "bar" }; + }; + } + } } diff --git a/spring-cloud-starter-function-web/pom.xml b/spring-cloud-starter-function-web/pom.xml index 169661b40..6e1ea0c5d 100644 --- a/spring-cloud-starter-function-web/pom.xml +++ b/spring-cloud-starter-function-web/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT .. spring-cloud-starter-function-web diff --git a/spring-cloud-starter-function-webflux/pom.xml b/spring-cloud-starter-function-webflux/pom.xml index 69b8e3e57..f82ab7a2b 100644 --- a/spring-cloud-starter-function-webflux/pom.xml +++ b/spring-cloud-starter-function-webflux/pom.xml @@ -6,7 +6,7 @@ org.springframework.cloud spring-cloud-function-parent - 3.2.2-SNAPSHOT + 3.2.13-SNAPSHOT spring-cloud-starter-function-webflux spring-cloud-starter-function-webflux