diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 000000000..0c4b142e9 --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 000000000..81f84f934 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,27 @@ +# This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/actions/stale +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '42 17 * * *' + workflow_dispatch: +jobs: + stale: + + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + + steps: + - uses: actions/stale@v5 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue has been stale for over 60 days' + stale-pr-message: 'This PR has been stale for over 60 days' + stale-issue-label: 'no-issue-activity' + stale-pr-label: 'no-pr-activity' diff --git a/.gitignore b/.gitignore index b2c31d2dc..31a040841 100644 --- a/.gitignore +++ b/.gitignore @@ -35,5 +35,7 @@ pom.xml.versionsBackup node node_modules build -package.json +/package.json package-lock.json +*samconfig.toml +*.aws-sam/ diff --git a/README.adoc b/README.adoc index cb06b375d..072334cd4 100644 --- a/README.adoc +++ b/README.adoc @@ -23,245 +23,91 @@ image::https://travis-ci.org/spring-cloud/spring-cloud-function.svg?branch={bran = Building :page-section-summary-toc: 1 -:spring-cloud-build-branch: main - -Spring Cloud is released under the non-restrictive Apache 2.0 license, -and follows a very standard Github development process, using Github -tracker for issues and merging pull requests into main. If you want -to contribute even something trivial please do not hesitate, but -follow the guidelines below. - -[[sign-the-contributor-license-agreement]] -== Sign the Contributor License Agreement - -Before we accept a non-trivial patch or pull request we will need you to sign the -https://cla.pivotal.io/sign/spring[Contributor License Agreement]. -Signing the contributor's agreement does not grant anyone commit rights to the main -repository, but it does mean that we can accept your contributions, and you will get an -author credit if we do. Active contributors might be asked to join the core team, and -given the ability to merge pull requests. - -[[code-of-conduct]] -== Code of Conduct -This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/src/main/asciidoc/code-of-conduct.adoc[code of -conduct]. By participating, you are expected to uphold this code. Please report -unacceptable behavior to spring-code-of-conduct@pivotal.io. - -[[code-conventions-and-housekeeping]] -== Code Conventions and Housekeeping -None of these is essential for a pull request, but they will all help. They can also be -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/main/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. -* Make sure all new `.java` files to have a simple Javadoc class comment with at least an - `@author` tag identifying you, and preferably at least a paragraph on what the class is - for. -* Add the ASF license header comment to all new `.java` files (copy from existing files - in the project) -* Add yourself as an `@author` to the .java files that you modify substantially (more - than cosmetic changes). -* Add some Javadocs and, if you change the namespace, some XSD doc elements. -* A few unit tests would help a lot as well -- someone has to do it. -* If no-one else is using your branch, please rebase it against the current main (or - other target branch in the main project). -* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], - if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit - message (where XXXX is the issue number). - -[[checkstyle]] -== Checkstyle - -Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are: - -.spring-cloud-build-tools/ ----- -└── src -    ├── checkstyle -    │   └── checkstyle-suppressions.xml <3> -    └── main -    └── resources -    ├── checkstyle-header.txt <2> -    └── checkstyle.xml <1> ----- -<1> Default Checkstyle rules -<2> File header setup -<3> Default suppression rules - -[[checkstyle-configuration]] -=== Checkstyle configuration - -Checkstyle rules are *disabled by default*. To add checkstyle to your project just define the following properties and plugins. - -.pom.xml ----- - -true <1> - true - <2> - true - <3> - - - - - <4> - io.spring.javaformat - spring-javaformat-maven-plugin - - <5> - org.apache.maven.plugins - maven-checkstyle-plugin - - - - - - <5> - org.apache.maven.plugins - maven-checkstyle-plugin - - - - ----- -<1> Fails the build upon Checkstyle errors -<2> Fails the build upon Checkstyle violations -<3> Checkstyle analyzes also the test sources -<4> Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules -<5> Add checkstyle plugin to your build and reporting phases - -If you need to suppress some rules (e.g. line length needs to be longer), then it's enough for you to define a file under `${project.root}/src/checkstyle/checkstyle-suppressions.xml` with your suppressions. Example: - -.projectRoot/src/checkstyle/checkstyle-suppresions.xml ----- - - - - - - ----- - -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/main/.editorconfig -o .editorconfig -$ touch .springformat -``` +:jdkversion: 17 -[[ide-setup]] -== IDE setup +[[basic-compile-and-test]] +== Basic Compile and Test -[[intellij-idea]] -=== Intellij IDEA +To build the source you will need to install JDK {jdkversion}. -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/main/spring-cloud-build-tools[Spring Cloud Build] project. +Spring Cloud uses Maven for most build-related activities, and you +should be able to get off the ground quite quickly by cloning the +project you are interested in and typing -.spring-cloud-build-tools/ ---- -└── src -    ├── checkstyle -    │   └── checkstyle-suppressions.xml <3> -    └── main -    └── resources -    ├── checkstyle-header.txt <2> -    ├── checkstyle.xml <1> -    └── intellij -       ├── Intellij_Project_Defaults.xml <4> -       └── Intellij_Spring_Boot_Java_Conventions.xml <5> +$ ./mvnw install ---- -<1> Default Checkstyle rules -<2> File header setup -<3> Default suppression rules -<4> Project defaults for Intellij that apply most of Checkstyle rules -<5> Project style conventions for Intellij that apply most of Checkstyle rules - -.Code style - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/assets/images/intellij-code-style.png[Code style] - -Go to `File` -> `Settings` -> `Editor` -> `Code style`. There click on the icon next to the `Scheme` section. There, click on the `Import Scheme` value and pick the `Intellij IDEA code style XML` option. Import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml` file. -.Inspection profiles - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/assets/images/intellij-inspections.png[Code style] - -Go to `File` -> `Settings` -> `Editor` -> `Inspections`. There click on the icon next to the `Profile` section. There, click on the `Import Profile` and import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml` file. - -.Checkstyle - -To have Intellij work with Checkstyle, you have to install the `Checkstyle` plugin. It's advisable to also install the `Assertions2Assertj` to automatically convert the JUnit assertions - -image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/assets/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/main/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/main/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/main/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. - -[[duplicate-finder]] -== Duplicate Finder - -Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath. - -[[duplicate-finder-configuration]] -=== Duplicate Finder configuration - -Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`. - -.pom.xml -[source,xml] ----- - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - +NOTE: You can also install Maven (>=3.3.3) yourself and run the `mvn` command +in place of `./mvnw` in the examples below. If you do that you also +might need to add `-P spring` if your local Maven settings do not +contain repository declarations for spring pre-release artifacts. + +NOTE: Be aware that you might need to increase the amount of memory +available to Maven by setting a `MAVEN_OPTS` environment variable with +a value like `-Xmx512m -XX:MaxPermSize=128m`. We try to cover this in +the `.mvn` configuration, so if you find you have to do it to make a +build succeed, please raise a ticket to get the settings added to +source control. + +The projects that require middleware (i.e. Redis) for testing generally +require that a local instance of [Docker](https://www.docker.com/get-started) is installed and running. + +[[documentation]] +== Documentation + +The spring-cloud-build module has a "docs" profile, and if you switch +that on it will try to build asciidoc sources using https://docs.antora.org/antora/latest/[Antora] from +`modules/ROOT/`. + +As part of that process it will look for a +`docs/src/main/asciidoc/README.adoc` and process it by loading all the includes, but not +parsing or rendering it, just copying it to `${main.basedir}` +(defaults to `$\{basedir}`, i.e. the root of the project). If there are +any changes in the README it will then show up after a Maven build as +a modified file in the correct place. Just commit it and push the change. + +[[working-with-the-code]] +== Working with the code +If you don't have an IDE preference we would recommend that you use +https://www.springsource.com/developer/sts[Spring Tools Suite] or +https://eclipse.org[Eclipse] when working with the code. We use the +https://eclipse.org/m2e/[m2eclipse] eclipse plugin for maven support. Other IDEs and tools +should also work without issue as long as they use Maven 3.3.3 or better. + +[[activate-the-spring-maven-profile]] +=== Activate the Spring Maven profile +Spring Cloud projects require the 'spring' Maven profile to be activated to resolve +the spring milestone and snapshot repositories. Use your preferred IDE to set this +profile to be active, or you may experience build errors. + +[[importing-into-eclipse-with-m2eclipse]] +=== Importing into eclipse with m2eclipse +We recommend the https://eclipse.org/m2e/[m2eclipse] eclipse plugin when working with +eclipse. If you don't already have m2eclipse installed it is available from the "eclipse +marketplace". + +NOTE: Older versions of m2e do not support Maven 3.3, so once the +projects are imported into Eclipse you will also need to tell +m2eclipse to use the right profile for the projects. If you +see many different errors related to the POMs in the projects, check +that you have an up to date installation. If you can't upgrade m2e, +add the "spring" profile to your `settings.xml`. Alternatively you can +copy the repository settings from the "spring" profile of the parent +pom into your `settings.xml`. + +[[importing-into-eclipse-without-m2eclipse]] +=== Importing into eclipse without m2eclipse +If you prefer not to use m2eclipse you can generate eclipse project metadata using the +following command: + +[indent=0] ---- - -For other properties, we have set defaults as listed in the https://github.com/basepom/duplicate-finder-maven-plugin/wiki[plugin documentation]. - -You can easily override them but setting the value of the selected property prefixed with `duplicate-finder-maven-plugin`. For example, set `duplicate-finder-maven-plugin.skip` to `true` in order to skip duplicates check in your build. - -If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your setup, make sure to add them in the plugin configuration section of your project: - -[source,xml] + $ ./mvnw eclipse:eclipse ---- - - - - org.basepom.maven - duplicate-finder-maven-plugin - - - org.joda.time.base.BaseDateTime - .*module-info - - - changelog.txt - - - - - - ----- +The generated eclipse projects can be imported by selecting `import existing projects` +from the `file` menu. [[contributing]] @@ -276,21 +122,17 @@ tracker for issues and merging pull requests into main. If you want to contribute even something trivial please do not hesitate, but follow the guidelines below. -[[sign-the-contributor-license-agreement]] -== Sign the Contributor License Agreement +[[developer-certificate-of-origin]] +== Developer Certificate of Origin (DCO) -Before we accept a non-trivial patch or pull request we will need you to sign the -https://cla.pivotal.io/sign/spring[Contributor License Agreement]. -Signing the contributor's agreement does not grant anyone commit rights to the main -repository, but it does mean that we can accept your contributions, and you will get an -author credit if we do. Active contributors might be asked to join the core team, and -given the ability to merge pull requests. +All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin. +For additional details, please refer to the blog post https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring]. [[code-of-conduct]] == Code of Conduct -This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/src/main/asciidoc/code-of-conduct.adoc[code of +This project adheres to the Contributor Covenant https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/modules/ROOT/partials/code-of-conduct.adoc[code of conduct]. By participating, you are expected to uphold this code. Please report -unacceptable behavior to spring-code-of-conduct@pivotal.io. +unacceptable behavior to code-of-conduct@spring.io. [[code-conventions-and-housekeeping]] == Code Conventions and Housekeeping @@ -464,7 +306,7 @@ Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, th [[duplicate-finder-configuration]] === Duplicate Finder configuration -Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`. +Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the project's `pom.xml`. .pom.xml [source,xml] diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml index 328a2e92a..158d51e05 100644 --- a/docs/antora-playbook.yml +++ b/docs/antora-playbook.yml @@ -1,13 +1,7 @@ antora: extensions: - - '@springio/antora-extensions/partial-build-extension' - - require: '@springio/antora-extensions/latest-version-extension' - - require: '@springio/antora-extensions/inject-collector-cache-config-extension' - - '@antora/collector-extension' - - '@antora/atlas-extension' - - require: '@springio/antora-extensions/root-component-extension' + - require: '@springio/antora-extensions' root_component_name: 'cloud-function' - - '@springio/antora-extensions/static-page-extension' site: title: Spring Cloud Function url: https://docs.spring.io/spring-cloud-function/reference/ @@ -36,4 +30,4 @@ runtime: format: pretty ui: bundle: - url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.11/ui-bundle.zip + url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip diff --git a/docs/modules/ROOT/assets/images/aws_spring_lambda_edit.png b/docs/modules/ROOT/assets/images/aws_spring_lambda_edit.png new file mode 100644 index 000000000..370aeaa80 Binary files /dev/null and b/docs/modules/ROOT/assets/images/aws_spring_lambda_edit.png differ diff --git a/docs/modules/ROOT/assets/images/aws_spring_lambda_test.png b/docs/modules/ROOT/assets/images/aws_spring_lambda_test.png new file mode 100644 index 000000000..99ed2392f Binary files /dev/null and b/docs/modules/ROOT/assets/images/aws_spring_lambda_test.png differ diff --git a/docs/modules/ROOT/pages/adapters/aws-intro.adoc b/docs/modules/ROOT/pages/adapters/aws-intro.adoc index 86419bdcf..946bbf8c0 100644 --- a/docs/modules/ROOT/pages/adapters/aws-intro.adoc +++ b/docs/modules/ROOT/pages/adapters/aws-intro.adoc @@ -1,18 +1,24 @@ [[aws-lambda]] = AWS Lambda +:page-aliases: adapters/aws.adoc The https://aws.amazon.com/[AWS] adapter takes a Spring Cloud Function app and converts it to a form that can run in AWS Lambda. -The details of how to get stared with AWS Lambda is out of scope of this document, so the expectation is that user has some familiarity with -AWS and AWS Lambda and wants to learn what additional value spring provides. + +In general, there are two ways to run Spring applications on AWS Lambda: + +1. Use the AWS Lambda adapter via Spring Cloud Function to implement a functional approach as outlined below. This is a good fit for single responsibility APIs and event & messaging-based systems such as handling messages from an Amazon SQS or Amazon MQ queue, an Apache Kafka stream, or reacting to file uploads in Amazon S3. +2. Run a Spring Boot Web application on AWS Lambda via the https://github.com/aws/serverless-java-container[Serverless Java container project]. This is a good fit for migrations of existing Spring applications to AWS Lambda or if you build sophisticated APIs with multiple API endpoints and want to maintain the familiar `RestController` approach. This approach is outlined in more detail in <>. + + +The following guide expects that you have a basic understanding of AWS and AWS Lambda and focuses on the additional value that Spring provides. The details on how to get started with AWS Lambda are out of scope of this document. If you want to learn more, you can navigate to https://docs.aws.amazon.com/lambda/latest/dg/concepts-basics.html[basic AWS Lambda concepts] or a complete https://catalog.workshops.aws/java-on-aws/[Java on AWS overview]. [[getting-started]] == Getting Started -One of the goals of Spring Cloud Function framework is to provide necessary infrastructure elements to enable a _simple function application_ -to interact in a certain way in a particular environment. -A simple function application (in context or Spring) is an application that contains beans of type Supplier, Function or Consumer. -So, with AWS it means that a simple function bean should somehow be recognised and executed in AWS Lambda environment. +One of the goals of Spring Cloud Function framework is to provide the necessary infrastructure elements to enable a _simple functional application_ to be compatible with a particular environment (such as AWS Lambda). + +In the context of Spring, a simple functional application contains beans of type `Supplier`, `Function` or `Consumer`. Let’s look at the example: @@ -32,93 +38,239 @@ public class FunctionConfiguration { } ---- -It shows a complete Spring Boot application with a function bean defined in it. What’s interesting is that on the surface this is just -another boot app, but in the context of AWS Adapter it is also a perfectly valid AWS Lambda application. No other code or configuration -is required. All you need to do is package it and deploy it, so let’s look how we can do that. +You can see a complete Spring Boot application with a function bean defined in it. On the surface this is just another Spring Boot app. However, when adding the Spring Cloud Function AWS Adapter to the project it will become a perfectly valid AWS Lambda application: + +[source, xml] +---- + + + + org.springframework.cloud + spring-cloud-function-adapter-aws + + +---- + +No other code or configuration is required. We’ve provided a sample project ready to be built and deployed. You can access it https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws[in the official Spring Cloud function example repository]. -To make things simpler we’ve provided a sample project ready to be built and deployed and you can access it -https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws[here]. +You simply execute `mvn clean package` to generate the JAR file. All the necessary maven plugins have already been setup to generate +an appropriate AWS deployable JAR file. (You can read more details about the JAR layout in <>). -You simply execute `./mvnw clean package` to generate JAR file. All the necessary maven plugins have already been setup to generate -appropriate AWS deployable JAR file. (You can read more details about JAR layout in <>). +[[aws-function-handlers]] +=== AWS Lambda Function Handler -Then you have to upload the JAR file (via AWS dashboard or AWS CLI) to AWS. +In contrast to traditional web applications that expose their functionality via a listener on a given HTTP port (80, 443), AWS Lambda functions are invoked at a predefined entry point, called the Lambda https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html[function handler]. -When ask about _handler_ you specify `org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest` which is a generic request handler. +We recommend using the built-in `org.springframework.cloud.function.adapter.aws.FunctionInvoker` handler to streamline the integration with AWS Lambda. It provides advanced features such as multi-function routing, decoupling from AWS specifics, and POJO serialization out of the box. Please refer to the <> and <> sections to learn more. -image::AWS-deploy.png[width=800,scaledwidth="75%",align="center"] +[[deployment-options]] +=== Deployment -That is all. Save and execute the function with some sample data which for this function is expected to be a -String which function will uppercase and return back. +After building the application, you can deploy the JAR file either manually via the AWS console, the AWS Command Line Interface (CLI), or Infrastructure as Code (IaC) tools such as https://aws.amazon.com/serverless/sam/[AWS Serverless Application Model (AWS SAM)], https://aws.amazon.com/cdk/[AWS Cloud Development Kit (AWS CDK)], https://aws.amazon.com/cloudformation/[AWS CloudFormation], or https://docs.aws.amazon.com/prescriptive-guidance/latest/choose-iac-tool/terraform.html[Terraform]. -While `org.springframework.cloud.function.adapter.aws.FunctionInvoker` is a general purpose AWS's `RequestHandler` implementation aimed at completely -isolating you from the specifics of AWS Lambda API, for some cases you may want to specify which specific AWS's `RequestHandler` you want -to use. The next section will explain you how you can accomplish just that. +To create a Hello world Lambda function with the AWS console +1. Open the https://console.aws.amazon.com/lambda/home#/functions[Functions page of the Lambda console]. +2. Choose _Create function_. +3. Select _Author from scratch_. +4. For Function name, enter `MySpringLambdaFunction`. +5. For Runtime, choose _Java 21_. +6. Choose _Create function_. + +To upload your code and test the function: + +1. Upload the previously created JAR file for example `target/function-sample-aws-0.0.1-SNAPSHOT-aws.jar`. + +2. Provide the entry handler method `org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest`. + +3. Navigate to the "Test" tab and click the "Test" button. The function should return with the provided JSON payload in uppercase. + +image::aws_spring_lambda_edit.png[width=800,scaledwidth="75%",align="center"] + +image::aws_spring_lambda_test.png[width=800,scaledwidth="75%",align="center"] + +To automate your deployment with Infrastructure as Code (IaC) tools please refer to https://docs.aws.amazon.com/lambda/latest/dg/foundation-iac.html[the official AWS documentation]. [[aws-request-handlers]] -=== AWS Request Handlers +== AWS Request Handlers -While AWS Lambda allows you to implement various `RequestHandlers`, with Spring Cloud Function you don't need to implement any, and instead use the provided - `org.springframework.cloud.function.adapter.aws.FunctionInvoker` which is the implementation of AWS's `RequestStreamHandler`. -User doesn't need to do anything other then specify it as 'handler' on AWS dashboard when deploying function. -It will handle most of the case including Kinesis, streaming etc. . +As discussed in the getting started section, AWS Lambda functions are invoked at a predefined entry point, called the https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html[Lambda function handler]. In its simplest form this can be a Java method reference. In the above example that would be `com.my.package.FunctionConfiguration::uppercase`. This configuration is needed to advise AWS Lambda which Java method to call in the provided JAR. + +When a Lambda function is invoked, it passes an additional request payload and context object to this handler method. The request payload varies based on the AWS service (Amazon API Gateway, Amazon S3, Amazon SQS, Apache Kafka etc.) that triggered the function. The context object provides additional information about the Lambda function, the invocation and the environment, for example a unique request id (https://docs.aws.amazon.com/lambda/latest/dg/java-context.html[see also Java context in the official documentation]). + +AWS provides predefined handler interfaces (called `RequestHandler` or `RequestStreamHandler`) to deal with payload and context objects via the aws-lambda-java-events and aws-lambda-java-core libraries. + +Spring Cloud Function already implements these interfaces and provides a `org.springframework.cloud.function.adapter.aws.FunctionInvoker` to completely abstract your function code +from the specifics of AWS Lambda. This allows you to just switch the entry point depending on which platform you run your functions. + +However, for some use cases you want to integrate deeply with the AWS environment. For example, when your function is triggered by an Amazon S3 file upload you might want to access specific Amazon S3 properties. Or, if you want to return a partial batch response when processing items from an Amazon SQS queue. In that case you can still leverage the generic `org.springframework.cloud.function.adapter.aws.FunctionInvoker` but you will work with the dedicated AWS objects from within your function code: + +[source, java] +---- +@Bean +public Function processS3Event() {} +@Bean +public Function processSQSEvent() {} -If your app has more than one `@Bean` of type `Function` etc. then you can choose the one to use by configuring `spring.cloud.function.definition` -property or environment variable. The functions are extracted from the Spring Cloud `FunctionCatalog`. In the event you don't specify `spring.cloud.function.definition` -the framework will attempt to find a default following the search order where it searches first for `Function` then `Consumer` and finally `Supplier`). +---- [[type-conversion]] === Type Conversion -Spring Cloud Function will attempt to transparently handle type conversion between the raw +Another benefit of leveraging the built-in `FunctionInvoker` is that Spring Cloud Function will attempt to transparently handle type conversion between the raw input stream and types declared by your function. -For example, if your function signature is as such `Function` we will attempt to convert -incoming stream event to an instance of `Foo`. +For example, if your function signature is `Function` it will attempt to convert the incoming stream event to an instance of `Foo`. This is especially helpful in API-triggered Lambda functions where the request body represents a business object and is not tied to AWS specifics. -In the event type is not known or can not be determined (e.g., `Function`) we will attempt to +If the event type is not known or can not be determined (e.g., `Function`) Spring Cloud Function will attempt to convert an incoming stream event to a generic `Map`. [[raw-input]] === Raw Input There are times when you may want to have access to a raw input. In this case all you need is to declare your -function signature to accept `InputStream`. For example, `Function`. In this case -we will not attempt any conversion and will pass the raw input directly to a function. - +function signature to accept `InputStream`, for example `Function`. +If specified, Spring Cloud function will not attempt any conversion and will pass the raw input directly to the function. [[aws-function-routing]] -=== AWS Function Routing +== AWS Function Routing + +One of the core features of Spring Cloud Function is https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[routing]. This capability allows you to have one special Java method (acting as a https://docs.aws.amazon.com/lambda/latest/dg/java-handler.html[Lambda function handler]) to delegate to other internal methods. You have already seen this in action when the generic `FunctionInvoker` automatically routed the requests to your `uppercase` function in the <> section. + +By default, if your app has more than one `@Bean` of type `Function` etc. they are extracted from the Spring Cloud `FunctionCatalog` and the framework will attempt to find a default following the search order where it searches first for `Function` then `Consumer` and finally `Supplier`. These default routing capabilities are needed because `FunctionInvoker` can not determine which function to bind, so it defaults internally to `RoutingFunction`. It is recommended to provide additional routing instructions https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[using several mechanisms] (see https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample] for more details). + +The right routing mechanism depends on your preference to deploy your Spring Cloud Function project as a single or multiple Lambda functions. + +[[aws-function-routing-single-multi]] +=== Single Function vs. Multiple Functions + +If you implement multiple Java methods in the same Spring Cloud Function project, for example `uppercase` and `lowercase`, you either deploy two separate Lambda functions with static routing information or you provide a dynamic routing method that decides which method to call during runtime. Let's look at both approaches. + +1. Deploying two separate AWS Lambda functions makes sense if you have different scaling, configuration or permission requirements per function. For example, if you create two Java methods `readObjectFromAmazonS3` and `writeToAmazonDynamoDB` in the same Spring Cloud Function project, you might want to create two separate Lambda functions. This is because they need different permissions to talk to either S3 or DynamoDB or their load pattern and memory configurations highly vary. In general, this approach is also recommended for messaging based applications where you read from a stream or a queue since you have a dedicated configuration per https://docs.aws.amazon.com/lambda/latest/dg/invocation-eventsourcemapping.html[Lambda Event Source mapping]. + +2. A single Lambda function is a valid approach when multiple Java methods share the same permission set or provide a cohesive business functionality. For example a CRUD-based Spring Cloud Function project with `createPet`, `updatePet`, `readPet` and `deletePet` methods that all talk to the same DynamoDB table and have a similar usage pattern. Using a single Lambda function will improve deployment simplicity, cohesion and code reuse for shared classes (`PetEntity`). In addition, it can reduce cold starts between sequential invocations because a `readPet` followed by `writePet` will most likely hit an already running https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html[Lambda execution environment]. When you build more sophisticated APIs however, or you want to leverage a `@RestController` approach you may also want to evaluate the <> option. + +If you favor the first approach you can also create two separate Spring Cloud Function projects and deploy them individually. This can be beneficial if different teams are responsible for maintaining and deploying the functions. However, in that case you need to deal with sharing cross-cutting concerns such as helper methods or entity classes between them. In general, we advise applying the same software modularity principles to your functional projects as you do for traditional web-based applications. For additional information on how to choose the right approach you can refer to https://aws.amazon.com/blogs/compute/comparing-design-approaches-for-building-serverless-microservices/[Comparing design approaches for serverless microservices]. + +After the decision has been made you can benefit from the following routing mechanisms. + +[[aws-function-routing-multi]] +=== Routing for multiple Lambda functions + +If you have decided to deploy your single Spring Cloud Function project (JAR) to multiple Lambda functions you need to provide a hint on which specific method to call, for example `uppercase` or `lowercase`. You can use https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html[AWS Lambda environment variables] to provide the routing instructions. + +Note that AWS does not allow dots `.` and/or hyphens `-` in the name of the environment variable. You can benefit from Spring Boot support and simply substitute 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`. + +Therefore, a configuration for a single Spring Cloud project with two methods deployed to separate AWS Lambda functions can look like this: + +[source, java] +---- +@SpringBootApplication +public class FunctionConfiguration { + + public static void main(String[] args) { + SpringApplication.run(FunctionConfiguration.class, args); + } + + @Bean + public Function uppercase() { + return value -> value.toUpperCase(); + } + + @Bean + public Function lowercase() { + return value -> value.toLowerCase(); + } +} +---- + +[source, yaml] +---- +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + MyUpperCaseLambda: + Type: AWS::Serverless::Function + Properties: + Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker + Runtime: java21 + MemorySize: 512 + CodeUri: target/function-sample-aws-0.0.1-SNAPSHOT-aws.jar + Environment: + Variables: + spring_cloud_function_definition: uppercase + + MyLowerCaseLambda: + Type: AWS::Serverless::Function + Properties: + Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker + Runtime: java21 + MemorySize: 512 + CodeUri: target/function-sample-aws-0.0.1-SNAPSHOT-aws.jar + Environment: + Variables: + spring_cloud_function_definition: lowercase + +---- + +You may ask - why not use the Lambda function handler and point the entry method directly to `uppercase` and `lowercase`? In a Spring Cloud Function project it is recommended to use the built-in `FunctionInvoker` as outlined in <>. Therefore, we provide the routing definition via the environment variables. + + +[[aws-function-routing-single]] +=== Routing within a single Lambda function + +If you have decided to deploy your Spring Cloud Function project with multiple methods (`uppercase` or `lowercase`) to a single Lambda function you need a more dynamic routing approach. Since `application.properties` and environment variables are defined at build or deployment time you can't use them for a single function scenario. In this case you can leverage `MessagingRoutingCallback` or `Message Headers` as outlined in the https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[Spring Cloud Function Routing section]. + +More details are available in the provided https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample]. + +[[performance]] +== Performance considerations + +A core characteristic of Serverless Functions is the ability to scale to zero and handle sudden traffic spikes. To handle requests AWS Lambda spins up https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html[new execution environments]. These environments need to be initialized, your code needs to be downloaded and a JVM + your application needs to start. This is also known as a cold-start. To reduce this cold-start time you can rely on the following mechanisms to optimize performance. + +1. Leverage AWS Lambda SnapStart to start your Lambda function from pre-initialized snapshots. +2. Tune the Memory Configuration via AWS Lambda Power Tuning to find the best tradeoff between performance and cost. +3. Follow AWS SDK Best Practices such as defining SDK clients outside the handler code or leverage more advanced priming techniques. +4. Implement additional Spring mechanisms to reduce Spring startup and initialization time such as https://github.com/spring-cloud/spring-cloud-function/blob/main/spring-cloud-function-samples/function-functional-sample-aws/src/main/java/example/FunctionConfiguration.java[functional bean registration]. + +Please refer to https://aws.amazon.com/blogs/compute/reducing-java-cold-starts-on-aws-lambda-functions-with-snapstart/[the official guidance] for more information. + +[[graalvm]] +== GraalVM Native Image + +Spring Cloud Function provides GraalVM Native Image support for functions running on AWS Lambda. Since GraalVM native images do not run on a traditional Java Virtual Machine (JVM) you must deploy your native Spring Cloud Function to an AWS Lambda custom runtime. The most notable difference is that you no longer provide a JAR file but the native-image and a bootstrap file with starting instructions bundled in a zip package: + +[source, text] +---- +lambda-custom-runtime.zip + |-- bootstrap + |-- function-sample-aws-native +---- -One of the core features of Spring Cloud Function is https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[routing] -- an ability to have one special function to delegate to other functions based on the user provided routing instructions. +Bootstrap file: -In AWS Lambda environment this feature provides one additional benefit, as it allows you to bind a single function (Routing Function) -as AWS Lambda and thus a single HTTP endpoint for API Gateway. So in the end you only manage one function and one endpoint, while benefiting -from many function that can be part of your application. +[source, text] +---- +#!/bin/sh -More details are available in the provided https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample], -yet few general things worth mentioning. +cd ${LAMBDA_TASK_ROOT:-.} -Routing capabilities will be enabled by default whenever there is more then one function in your application as `org.springframework.cloud.function.adapter.aws.FunctionInvoker` -can not determine which function to bind as AWS Lambda, so it defaults to `RoutingFunction`. -This means that all you need to do is provide routing instructions which you can do https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[using several mechanisms] -(see https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample] for more details). +./function-sample-aws-native +---- -Also, note that since AWS does not allow dots `.` and/or hyphens`-` in the name of the environment variable, you can benefit from boot support and simply substitute -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`. +You can find https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-native[a full GraalVM native-image example with Spring Cloud Function on GitHub]. For a deep dive you can also refer to the https://catalog.workshops.aws/java-on-aws-lambda/en-US/02-accelerate/graal-plain-java[GraalVM modules of the Java on AWS Lambda workshop]. [[custom-runtime]] -=== Custom Runtime +== Custom Runtime + +Lambda focuses on providing stable long-term support (LTS) Java runtime versions. The official Lambda runtimes are built around a combination of operating system, programming language, and software libraries that are subject to maintenance and security updates. For example, the Lambda runtime for Java supports the LTS versions such as Java 17 Corretto and Java 21 Corretto. You can find the full list https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html[here]. There is no provided runtime for non-LTS versions like Java 22, Java 23 or Java 24. -You can also benefit from https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html[AWS Lambda custom runtime] feature of AWS Lambda -and Spring Cloud Function provides all the necessary components to make it easy. +To use other language versions, JVMs or GraalVM native-images, Lambda allows you to https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html[create custom runtimes]. Custom runtimes allow you to provide and configure your own runtimes for running their application code. Spring Cloud Function provides all the necessary components to make it easy. -From the code perspective the application should look no different then any other Spring Cloud Function application. -The only thing you need to do is to provide a `bootstrap` script in the root of your zip/jar that runs the Spring Boot application. +From the code perspective the application should not look different from any other Spring Cloud Function application. +The only thing you need to do is to provide a `bootstrap` script in the root of your ZIP/ JAR that runs the Spring Boot application. and select "Custom Runtime" when creating a function in AWS. Here is an example 'bootstrap' file: ```text @@ -133,28 +285,29 @@ java -Dspring.main.web-application-type=none -Dspring.jmx.enabled=false \ ``` The `com.example.LambdaApplication` represents your application which contains function beans. -Set the handler name in AWS to the name of your function. You can use function composition here as well (e.g., `uppecrase|reverse`). -That is pretty much all. Once you upload your zip/jar to AWS your function will run in custom runtime. -We provide a https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws-custom-new[sample project] -where you can also see how to configure yoru POM to properly generate the zip file. +Set the handler name in AWS to the name of your function. You can use function composition here as well (e.g., `uppercase|reverse`). +Once you upload your ZIP/ JAR to AWS your function will run in a custom runtime. +We provide a https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws-custom-new[sample project] +where you can also see how to configure your POM to properly generate the ZIP file. -The functional bean definition style works for custom runtimes as well, and is -faster than the `@Bean` style. A custom runtime can start up much quicker even than a functional bean implementation -of a Java lambda - it depends mostly on the number of classes you need to load at runtime. -Spring doesn't do very much here, so you can reduce the cold start time by only using primitive types in your function, for instance, +The functional bean definition style works for custom runtimes as well, and is +faster than the `@Bean` style. A custom runtime can start up much quicker even than a functional bean implementation +of a Java lambda - it depends mostly on the number of classes you need to load at runtime. +Spring doesn't do very much here, so you can reduce the cold start time by only using primitive types in your function, for instance, and not doing any work in custom `@PostConstruct` initializers. [[aws-function-routing-with-custom-runtime]] === 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. +When using a <> 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. -=== Deploying Container images +== Deploying Lambda functions as container images -Custom Runtime is also responsible for handling of container image deployments. -When deploying container images in a way similar to the one described https://github.com/spring-cloud/spring-cloud-function/issues/1021[here], it is important +In contrast to JAR or ZIP based deployments you can also deploy your Lambda functions as a container image via an image registry. For additional details please refer to the https://docs.aws.amazon.com/lambda/latest/dg/images-create.html[official AWS Lambda documentation]. + +When deploying container images in a way similar to the one described https://github.com/spring-cloud/spring-cloud-function/issues/1021[here], it is important to remember to set and environment variable `DEFAULT_HANDLER` with the name of the function. For example, for function bean shown below the `DEFAULT_HANDLER` value would be `readMessageFromSQS`. @@ -166,7 +319,7 @@ public Consumer> readMessageFromSQS() { } ---- -Also, it is important to remember to ensure tht `spring_cloud_function_web_export_enabled` is also set to `false`. It is by default. +Also, it is important to remember to ensure that `spring_cloud_function_web_export_enabled` is also set to `false`. It is `true` by default. [[notes-on-jar-layout]] == Notes on JAR Layout @@ -195,7 +348,7 @@ then additional transformers must be configured as part of the maven-shade-plugi org.springframework.boot spring-boot-maven-plugin - 2.7.4 + 3.4.2 @@ -237,7 +390,7 @@ then additional transformers must be configured as part of the maven-shade-plugi == Build file setup In order to run Spring Cloud Function applications on AWS Lambda, you can leverage Maven or Gradle - plugins offered by the cloud platform provider. +plugins. [[maven]] @@ -304,7 +457,7 @@ Below is a complete gradle file ---- plugins { id 'java' - id 'org.springframework.boot' version '3.2.0-M2' + id 'org.springframework.boot' version '3.4.2' id 'io.spring.dependency-management' version '1.1.3' id 'com.github.johnrengelman.shadow' version '8.1.1' id 'maven-publish' @@ -325,7 +478,7 @@ repositories { } ext { - set('springCloudVersion', "2023.0.0-M1") + set('springCloudVersion', "2024.0.0") } assemble.dependsOn = [thinJar, shadowJar] @@ -389,3 +542,60 @@ tasks.named('test') { You can find the entire sample `build.gradle` file for deploying Spring Cloud Function applications to AWS Lambda with Gradle https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws/build.gradle[here]. + +[[serverless-java-container]] +== Serverless Java container for Spring Boot Web + +You can use the https://github.com/aws/serverless-java-container[aws-serverless-java-container] library to run a Spring Boot 3 applications in AWS Lambda. This is a good fit for migrations of existing Spring applications to AWS Lambda or if you build sophisticated APIs with multiple API endpoints and want to maintain the familiar `RestController` approach. The following section provides a high-level overview of the process. Please refer to the https://github.com/aws/serverless-java-container/wiki/Quick-start---Spring-Boot3[official sample code for additional information]. + +1. Import the Serverless Java Container library to your existing Spring Boot 3 web app ++ +[source, java] +---- + + com.amazonaws.serverless + aws-serverless-java-container-springboot3 + 2.1.2 + +---- + +2. Use the built-in Lambda function handler that serves as an entrypoint ++ +`com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler` + +3. Configure an environment variable named `MAIN_CLASS` to let the generic handler know where to find your original application main class. Usually that is the class annotated with @SpringBootApplication. + +`MAIN_CLAS = com.my.package.MySpringBootApplication` + +Below you can see an example deployment configuration: + +[source, yaml] +---- +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 + +Resources: + MySpringBootLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler + Runtime: java21 + MemorySize: 1024 + CodeUri: target/lambda-spring-boot-app-0.0.1-SNAPSHOT.jar #Must be a shaded Jar + Environment: + Variables: + MAIN_CLASS: com.amazonaws.serverless.sample.springboot3.Application #Class annotated with @SpringBootApplication + +---- + +Please find all the examples including GraalVM native-image https://github.com/aws/serverless-java-container/tree/main/samples/springboot3[here]. + + +[[resources]] +== Additional resources + +- https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples[Official Example Repositories on GitHub] +- https://catalog.workshops.aws/java-on-aws-lambda/en-US/01-migration/architecture-overview[Java on AWS Lambda workshop with dedicated Spring examples] +- https://catalog.workshops.aws/java-on-aws/en-US[Java on AWS Immersion Day] +- https://serverlessland.com/content/service/lambda/paved-path/java-replatforming/introduction[Java Replatforming Guide] +- https://www.youtube.com/watch?v=AFIHug_HujI[Talk: Spring I/O 2024 - Serverless Java with Spring] diff --git a/docs/modules/ROOT/pages/adapters/aws.adoc b/docs/modules/ROOT/pages/adapters/aws.adoc deleted file mode 100644 index fe35449d0..000000000 --- a/docs/modules/ROOT/pages/adapters/aws.adoc +++ /dev/null @@ -1,155 +0,0 @@ -*{project-version}* - - -The https://aws.amazon.com/[AWS] adapter takes a Spring Cloud Function app and converts it to a form that can run in AWS Lambda. - -[[introduction]] -== Introduction - -The details of how to get stared with AWS Lambda is out of scope of this document, so the expectation is that user has some familiarity with -AWS and AWS Lambda and wants to learn what additional value spring provides. - - -=== Getting Started - -One of the goals of Spring Cloud Function framework is to provide necessary infrastructure elements to enable a _simple function application_ -to interact in a certain way in a particular environment. -A simple function application (in context or Spring) is an application that contains beans of type Supplier, Function or Consumer. -So, with AWS it means that a simple function bean should somehow be recognised and executed in AWS Lambda environment. - -Let’s look at the example: - -[source, java] ----- -@SpringBootApplication -public class FunctionConfiguration { - - public static void main(String[] args) { - SpringApplication.run(FunctionConfiguration.class, args); - } - - @Bean - public Function uppercase() { - return value -> value.toUpperCase(); - } -} ----- - -It shows a complete Spring Boot application with a function bean defined in it. What’s interesting is that on the surface this is just -another boot app, but in the context of AWS Adapter it is also a perfectly valid AWS Lambda application. No other code or configuration -is required. All you need to do is package it and deploy it, so let’s look how we can do that. - -To make things simpler we’ve provided a sample project ready to be built and deployed and you can access it -https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws[here]. - -You simply execute `./mvnw clean package` to generate JAR file. All the necessary maven plugins have already been setup to generate -appropriate AWS deployable JAR file. (You can read more details about JAR layout in <>). - -Then you have to upload the JAR file (via AWS dashboard or AWS CLI) to AWS. - -When ask about _handler_ you specify `org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest` which is a generic request handler. - -image::AWS-deploy.png[width=800,scaledwidth="75%",align="center"] - -That is all. Save and execute the function with some sample data which for this function is expected to be a -String which function will uppercase and return back. - -While `org.springframework.cloud.function.adapter.aws.FunctionInvoker` is a general purpose AWS's `RequestHandler` implementation aimed at completely -isolating you from the specifics of AWS Lambda API, for some cases you may want to specify which specific AWS's `RequestHandler` you want -to use. The next section will explain you how you can accomplish just that. - - -[[aws-request-handlers]] -== AWS Request Handlers - -The adapter has a couple of generic request handlers that you can use. The most generic is (and the one we used in the Getting Started section) -is `org.springframework.cloud.function.adapter.aws.FunctionInvoker` which is the implementation of AWS's `RequestStreamHandler`. -User doesn't need to do anything other then specify it as 'handler' on AWS dashboard when deploying function. -It will handle most of the case including Kinesis, streaming etc. . - - -If your app has more than one `@Bean` of type `Function` etc. then you can choose the one to use by configuring `spring.cloud.function.definition` -property or environment variable. The functions are extracted from the Spring Cloud `FunctionCatalog`. In the event you don't specify `spring.cloud.function.definition` -the framework will attempt to find a default following the search order where it searches first for `Function` then `Consumer` and finally `Supplier`). - -[[aws-context]] -== 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. - - -[[aws-function-routing]] -== AWS Function Routing - -One of the core features of Spring Cloud Function is https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[routing] -- an ability to have one special function to delegate to other functions based on the user provided routing instructions. - -In AWS Lambda environment this feature provides one additional benefit, as it allows you to bind a single function (Routing Function) -as AWS Lambda and thus a single HTTP endpoint for API Gateway. So in the end you only manage one function and one endpoint, while benefiting -from many function that can be part of your application. - -More details are available in the provided https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample], -yet few general things worth mentioning. - -Routing capabilities will be enabled by default whenever there is more then one function in your application as `org.springframework.cloud.function.adapter.aws.FunctionInvoker` -can not determine which function to bind as AWS Lambda, so it defaults to `RoutingFunction`. -This means that all you need to do is provide routing instructions which you can do https://docs.spring.io/spring-cloud-function/docs/{project-version}/reference/html/spring-cloud-function.html#_function_routing_and_filtering[using several mechanisms] -(see https://github.com/spring-cloud/spring-cloud-function/tree/main/spring-cloud-function-samples/function-sample-aws-routing[sample] for more details). - -Also, note that since AWS does not allow dots `.` and/or hyphens`-` in the name of the environment variable, you can benefit from boot support and simply substitute -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`. - - -[[http-and-api-gateway]] -== HTTP and API Gateway - -AWS has some platform-specific data types, including batching of messages, which is much more efficient than processing each one individually. To make use of these types you can write a function that depends on those types. Or you can rely on Spring to extract the data from the AWS types and convert it to a Spring `Message`. To do this you tell AWS that the function is of a specific generic handler type (depending on the AWS service) and provide a bean of type `Function,Message>`, where `S` and `T` are your business data types. If there is more than one bean of type `Function` you may also need to configure the Spring Boot property `function.name` to be the name of the target bean (e.g. use `FUNCTION_NAME` as an environment variable). - -The supported AWS services and generic handler types are listed below: - -|=== -| Service | AWS Types | Generic Handler | - -| API Gateway | `APIGatewayProxyRequestEvent`, `APIGatewayProxyResponseEvent` | `org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler` | -| Kinesis | KinesisEvent | org.springframework.cloud.function.adapter.aws.SpringBootKinesisEventHandler | -|=== - - -For example, to deploy behind an API Gateway, use `--handler org.springframework.cloud.function.adapter.aws.SpringBootApiGatewayRequestHandler` in your AWS command line (in via the UI) and define a `@Bean` of type `Function,Message>` where `Foo` and `Bar` are POJO types (the data will be marshalled and unmarshalled by AWS using Jackson). - -[[custom-runtime]] -== Custom Runtime - -You can also benefit from https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html[AWS Lambda custom runtime] feature of AWS Lambda -and Spring Cloud Function provides all the necessary components to make it easy. - -From the code perspective the application should look no different then any other Spring Cloud Function application. -The only thing you need to do is to provide a `bootstrap` script in the root of your zip/jar that runs the Spring Boot application. -and select "Custom Runtime" when creating a function in AWS. -Here is an example 'bootstrap' file: -```text -#!/bin/sh - -cd ${LAMBDA_TASK_ROOT:-.} - -java -Dspring.main.web-application-type=none -Dspring.jmx.enabled=false \ - -noverify -XX:TieredStopAtLevel=1 -Xss256K -XX:MaxMetaspaceSize=128M \ - -Djava.security.egd=file:/dev/./urandom \ - -cp .:`echo lib/*.jar | tr ' ' :` com.example.LambdaApplication -``` -The `com.example.LambdaApplication` represents your application which contains function beans. - -Set the handler name in AWS to the name of your function. You can use function composition here as well (e.g., `uppecrase|reverse`). -That is pretty much all. Once you upload your zip/jar to AWS your function will run in custom runtime. -We provide a https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-aws-custom-new[sample project] -where you can also see how to configure yoru POM to properly generate the zip file. - -The functional bean definition style works for custom runtimes as well, and is -faster than the `@Bean` style. A custom runtime can start up much quicker even than a functional bean implementation -of a Java lambda - it depends mostly on the number of classes you need to load at runtime. -Spring doesn't do very much here, so you can reduce the cold start time by only using primitive types in your function, for instance, -and not doing any work in custom `@PostConstruct` initializers. diff --git a/docs/modules/ROOT/pages/adapters/azure-intro.adoc b/docs/modules/ROOT/pages/adapters/azure-intro.adoc index c8d0d8610..fa3ac447e 100644 --- a/docs/modules/ROOT/pages/adapters/azure-intro.adoc +++ b/docs/modules/ROOT/pages/adapters/azure-intro.adoc @@ -1,6 +1,6 @@ [[microsoft-azure-functions]] = Microsoft Azure Functions - +:page-aliases: adapters/azure.adoc https://azure.microsoft.com[Azure] function adapter for deploying `Spring Cloud Function` applications as native Azure Java Functions. @@ -198,7 +198,7 @@ Usually the Azure Maven (or Gradle) plugins are used to generate the necessary c IMPORTANT: The Azure https://learn.microsoft.com/en-us/azure/azure-functions/functions-reference-java?tabs=bash%2Cconsumption#folder-structure[packaging format] is not compatible with the default Spring Boot packaging (e.g. `uber jar`). The xref:adapters/azure-intro.adoc#disable.spring.boot.plugin[Disable Spring Boot Plugin] section below explains how to handle this. -[[azure-maven/gradle-plugins]] +[[azure-maven-gradle-plugins]] === Azure Maven/Gradle Plugins Azure provides https://github.com/microsoft/azure-maven-plugins/tree/develop/azure-functions-maven-plugin[Maven] and https://github.com/microsoft/azure-gradle-plugins/tree/master/azure-functions-gradle-plugin[Gradle] plugins to process the annotated classes, generate the necessary configurations and produce the expected package layout. diff --git a/docs/modules/ROOT/pages/adapters/azure.adoc b/docs/modules/ROOT/pages/adapters/azure.adoc deleted file mode 100644 index 490093572..000000000 --- a/docs/modules/ROOT/pages/adapters/azure.adoc +++ /dev/null @@ -1,2 +0,0 @@ -*{project-version}* - diff --git a/docs/modules/ROOT/pages/adapters/gcp-intro.adoc b/docs/modules/ROOT/pages/adapters/gcp-intro.adoc index 04480c856..02a92c3d7 100644 --- a/docs/modules/ROOT/pages/adapters/gcp-intro.adoc +++ b/docs/modules/ROOT/pages/adapters/gcp-intro.adoc @@ -1,5 +1,6 @@ [[google-cloud-functions]] = Google Cloud Functions +:page-aliases: adapters/gcp.adoc The Google Cloud Functions adapter enables Spring Cloud Function apps to run on the https://cloud.google.com/functions[Google Cloud Functions] serverless platform. You can either run the function locally using the open source https://github.com/GoogleCloudPlatform/functions-framework-java[Google Functions Framework for Java] or on GCP. @@ -109,7 +110,7 @@ curl http://localhost:8080/ -d "hello" ---- -== Buikd & Deploy to GCP +== Build & Deploy to GCP Start by packaging your application. diff --git a/docs/modules/ROOT/pages/adapters/gcp.adoc b/docs/modules/ROOT/pages/adapters/gcp.adoc deleted file mode 100644 index 490093572..000000000 --- a/docs/modules/ROOT/pages/adapters/gcp.adoc +++ /dev/null @@ -1,2 +0,0 @@ -*{project-version}* - diff --git a/docs/modules/ROOT/pages/spring-cloud-function/deploying-a-packaged.adoc b/docs/modules/ROOT/pages/spring-cloud-function/deploying-a-packaged.adoc index a3b6ce4a1..932d502b7 100644 --- a/docs/modules/ROOT/pages/spring-cloud-function/deploying-a-packaged.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-function/deploying-a-packaged.adoc @@ -19,7 +19,7 @@ the functions. It can optionally use a `maven:` prefix to locate the artifact vi for complete details). A Spring Boot application is bootstrapped from the jar file, using the `MANIFEST.MF` to locate a start class, so that a standard Spring Boot fat jar works well, for example. If the target jar can be launched successfully then the result is a function registered in the main application's `FunctionCatalog`. The registered function can be applied by code in the main application, even though -it was created in an isolated class loader (by deault). +it was created in an isolated class loader (by default). Here is the example of deploying a JAR which contains an 'uppercase' function and invoking it . diff --git a/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc b/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc index 9b5df6350..f371dc11a 100644 --- a/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-function/programming-model.adoc @@ -2,67 +2,71 @@ = Programming model [[function.catalog]] - [[function-catalog-and-flexible-function-signatures]] == Function Catalog and Flexible Function Signatures -One of the main features of Spring Cloud Function is to adapt and support a range of type signatures for user-defined functions, -while providing a consistent execution model. -That's why all user defined functions are transformed into a canonical representation by `FunctionCatalog`. +One of the main features of Spring Cloud Function is to adapt and support a range of type signatures for user-defined functions, while providing a consistent execution model. +That's why all user-defined functions are transformed into a canonical representation by `FunctionCatalog`. -While users don't normally have to care about the `FunctionCatalog` at all, it is useful to know what -kind of functions are supported in user code. +While users don't normally have to care about the `FunctionCatalog` at all, it is useful to know what kind of functions are supported in user code. -It is also important to understand that Spring Cloud Function provides first class support for reactive API -provided by https://projectreactor.io/[Project Reactor] allowing reactive primitives such as `Mono` and `Flux` -to be used as types in user defined functions providing greater flexibility when choosing programming model for -your function implementation. -Reactive programming model also enables functional support for features that would be otherwise difficult to impossible to implement -using imperative programming style. For more on this please read <> section. +It is also important to understand that Spring Cloud Function provides first-class support for reactive APIs, provided by https://projectreactor.io/[Project Reactor]. +This allows reactive primitives such as `Mono` and `Flux` to be used as types in user-defined functions thereby providing greater flexibility when choosing a programming model for your function implementation. +A reactive programming model also enables functional support for features that would be otherwise difficult or impossible to implement using an imperative programming style. +For more on this, please read the section on <>. [[java-8-function-support]] == Java 8 function support -Spring Cloud Function embraces and builds on top of the 3 core functional interfaces defined by Java -and available to us since Java 8. +Spring Cloud Function embraces and builds on top of the 3 core functional interfaces defined by Java since Java 8. - Supplier - Function - Consumer -To avoid constantly mentioning `Supplier`, `Function` and `Consumer` we’ll refer to them a Functional beans for the rest of this manual where appropriate. +To constantly avoid mentioning `Supplier`, `Function` and `Consumer`, we’ll refer to them as Functional beans where appropriate for the rest of this manual. -In a nutshell, any bean in your Application Context that is Functional bean will lazily be registered with `FunctionCatalog`. +In a nutshell, any bean in your `ApplicationContext` that is a Functional bean will be lazily registered with `FunctionCatalog`. This means that it could benefit from all of the additional features described in this reference manual. -In a simplest of application all you need to do is to declare `@Bean` of type `Supplier`, `Function` or `Consumer` in your application configuration. -Then you can access `FunctionCatalog` and lookup a particular function based on its name. +In the simplest application, all you need to do is to declare a `@Bean` of type `Supplier`, `Function` or `Consumer` in your application configuration. +Then, you can use `FunctionCatalog` to lookup a particular function based on its name. For example: - -[source, test] +[source, java] ---- @Bean public Function uppercase() { return value -> value.toUpperCase(); } -. . . +// . . . FunctionCatalog catalog = applicationContext.getBean(FunctionCatalog.class); Function uppercase = catalog.lookup(“uppercase”); ---- -Important to understand that given that `uppercase` is a bean, you can certainly get it form the `ApplicationContext` directly, but all you will get is just your bean as you declared it without any extra features provided by SCF. When you do lookup of a function via `FunctionCatalog`, the instance you will receive is wrapped (instrumented) with additional features (i.e., type conversion, composition etc.) described in this manual. Also, it is important to understand that a typical user does not use Spring Cloud Function directly. Instead a typical user implements Java `Function/Supplier/Consumer` with the idea of using it in different execution contexts without additional work. For example the same java function could be represented as _REST endpoint_ or _Streaming message handler_ or _AWS Lambda_ and more via Spring Cloud Function provided -adapters as well as other frameworks using Spring Cloud Function as the core programming model (e.g., https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream]) -So in summary Spring Cloud Function instruments java functions with additional features to be utilised in variety of execution contexts. +It is important to understand that given `uppercase` is a bean, you can certainly get it form the `ApplicationContext` directly, but all you will get is just your bean as you declared it without any extra features provided by SCF. +When you look up a function via `FunctionCatalog`, the instance you receive is wrapped (instrumented) with additional features (i.e., type conversion, composition, etc.) described in this manual. + +Also, it is important to understand that a typical user does not use Spring Cloud Function directly. +Instead, a typical user implements a Java `Function`, `Supplier`, or `Consumer` with the idea of using it in different execution contexts without additional work. +For example, the same Java function could be represented as a _REST endpoint_, a _Streaming message handler_, or an _AWS Lambda_, and even more, via Spring Cloud Function provided adapters as well as other frameworks using Spring Cloud Function as the core programming model (e.g. https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream]). + +In summary, Spring Cloud Function instruments Java functions with additional features to be utilized in variety of execution contexts. [[function-definition]] === Function definition -While the previous example shows you how to lookup function in FunctionCatalog programmatically, in a typical integration case where Spring Cloud Function used as programming model by another framework (e.fg. Spring Cloud Stream), you declare which functions to use via `spring.cloud.function.definition` property. Knowing that it is important to understand some default behaviour when it comes to discovering functions in `FunctionCatalog`. For example, if you only have one Functional bean in your `ApplicationContext`, the `spring.cloud.function.definition` property typically will not be required, since a single function in `FunctionCatalog` can be looked up by an empty name or any name. For example, assuming that `uppercase` is the only function in your catalog, it can be looked up as `catalog.lookup(null)`, `catalog.lookup(“”)`, `catalog.lookup(“foo”)` -That said, for cases where you are using framework such as Spring Cloud Stream which uses `spring.cloud.function.definition` it is best practice and recommended to always use `spring.cloud.function.definition` property. + +While the previous example shows you how to lookup a function in `FunctionCatalog` programmatically, in a typical integration case where Spring Cloud Function is used as the programming model by another framework (e.g. https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream]), you can declare which functions to use via the `spring.cloud.function.definition` property. +It is important to know and understand the default behaviour when it comes to discovering functions in `FunctionCatalog`. + +For instance, if you only have one Functional bean in your `ApplicationContext`, the `spring.cloud.function.definition` property typically will not be required since a single function in `FunctionCatalog` can be looked up by an empty name, or any name. +For example, assuming that `uppercase` is the only function in your catalog, it can be looked up as `catalog.lookup(null)`, `catalog.lookup(“”)`, `catalog.lookup(“foo”)`. + +That said, for cases where you are using a framework such as Spring Cloud Stream, which uses `spring.cloud.function.definition`, it is recommended to always use the `spring.cloud.function.definition` property. For example, @@ -73,11 +77,12 @@ spring.cloud.function.definition=uppercase [[filtering-ineligible-functions]] === 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. + +A typical `ApplicationContext` may include beans that are valid Java functions, but not intended as candidates to be registered with `FunctionCatalog`. +Such beans could be auto-configurations from other projects or any other bean that qualifies as a Java function. + +The framework provides default filtering of known beans that should not be candidates for registration with `FunctionCatalog`. +You can also add additional beans to this list by providing a comma-delimited list of bean definition names using the `spring.cloud.function.ineligible-definitions` property. For example, @@ -88,24 +93,22 @@ spring.cloud.function.ineligible-definitions=foo,bar [[supplier]] === Supplier -Supplier can be _reactive_ - `Supplier>` -or _imperative_ - `Supplier`. From the invocation standpoint this should make no difference -to the implementor of such Supplier. However, when used within frameworks -(e.g., https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream]), Suppliers, especially reactive, -often used to represent the source of the stream, therefore they are invoked once to get the stream (e.g., Flux) -to which consumers can subscribe to. In other words such suppliers represent an equivalent of an _infinite stream_. -However, the same reactive suppliers can also represent _finite_ stream(s) (e.g., result set on the polled JDBC data). -In those cases such reactive suppliers must be hooked up to some polling mechanism of the underlying framework. +Supplier can be _reactive_ - `Supplier>` or _imperative_ - `Supplier`. +From an invocation standpoint, this should make no difference to the implementor of such a `Supplier`. + +However, when used within frameworks (e.g. https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream]), Suppliers, especially reactive, are often used to represent the source of a stream. +Therefore, they are invoked once to get the stream (e.g. `Flux`) to which consumers can subscribe. +In other words, such suppliers represent an equivalent of an _infinite stream_. + +Although, the same reactive suppliers can also represent a _finite_ stream (e.g. result set on polled JDBC data). +In those cases, such reactive suppliers must be hooked up to some polling mechanism of the underlying framework. -To assist with that Spring Cloud Function provides a marker annotation -`org.springframework.cloud.function.context.PollableBean` to signal that such supplier produces a -finite stream and may need to be polled again. That said, it is important to understand that Spring Cloud Function itself -provides no behavior for this annotation. +To assist with that Spring Cloud Function provides a marker annotation `org.springframework.cloud.function.context.PollableBean` to signal that such supplier produces a finite stream and may need to be polled again. +However, it is important to understand that Spring Cloud Function itself provides no behavior for this annotation. -In addition `PollableBean` annotation exposes a _splittable_ attribute to signal that produced stream -needs to be split (see https://www.enterpriseintegrationpatterns.com/patterns/messaging/Sequencer.html[Splitter EIP]) +In addition, the `PollableBean` annotation exposes a _splittable_ attribute to signal that the produced stream needs to be split (see https://www.enterpriseintegrationpatterns.com/patterns/messaging/Sequencer.html[Splitter EIP]) -Here is the example: +Here is an example: [source, java] ---- @@ -122,70 +125,102 @@ public Supplier> someSupplier() { [[function]] === Function -Function can also be written in imperative or reactive way, yet unlike Supplier and Consumer there are -no special considerations for the implementor other then understanding that when used within frameworks -such as https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream] and others, reactive function is -invoked only once to pass a reference to the stream (Flux or Mono) and imperative is invoked once per event. + +Functions can also be written in an imperative or reactive way. +Yet, unlike `Supplier` and `Consumer`, there are no special considerations for the implementor other then understanding that when used within frameworks, such as https://spring.io/projects/spring-cloud-stream[Spring Cloud Stream], a reactive function is invoked only once to pass a reference to the stream (i.e. `Flux` or `Mono`) whereas an imperative function is invoked once per event. + +[source, java] +---- +public Function uppercase() { + . . . . +} +---- + +[[bifunction]] +=== BiFunction + +In the event you need to receive some additional data (metadata) with your payload, you can always declare your function signature to receive a `Message` containing a map of headers with additional information. + +[source, java] +---- +public Function, String> uppercase() { + . . . . +} +---- + +To make your function signature a bit lighter and more POJO-like, there is another approach. You can use `BiFunction`. + +[source, java] +---- +public BiFunction uppercase() { + . . . . +} +---- + +Given that a `Message` only contains two attributes (payload and headers), and a `BiFunction` requires two input parameters, the framework will automatically recognise this signature and extract the payload from the `Message` passing it as a first argument and a `Map` of headers as the second. +As a result, your function is not coupled to Spring’s messaging API. +Keep in mind that `BiFunction` requires a strict signature where the second argument *must* be a `Map`. +The same rule applies to `BiConsumer`. [[consumer]] === Consumer -Consumer is a little bit special because it has a `void` return type, -which implies blocking, at least potentially. Most likely you will not -need to write `Consumer>`, but if you do need to do that, -remember to subscribe to the input flux. + +Consumer is a little bit special because it has a `void` return type, which implies blocking, at least potentially. +Most likely you will not need to write `Consumer>`, but if you do need to do that, remember to subscribe to the input `Flux`. [[function-composition]] == Function Composition + Function Composition is a feature that allows one to compose several functions into one. -The core support is based on function composition feature available with https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html#andThen-java.util.function.Function-[Function.andThen(..)] -support available since Java 8. However on top of it, we provide few additional features. +The core support is based on the function composition feature provided by https://docs.oracle.com/javase/8/docs/api/java/util/function/Function.html#andThen-java.util.function.Function-[Function.andThen(..)], available since Java 8. +However, Spring Cloud Function provides a few additional features on top of this. [[declarative-function-composition]] === Declarative Function Composition -This feature allows you to provide composition instruction in a declarative way using `|` (pipe) or `,` (comma) delimiter -when providing `spring.cloud.function.definition` property. +This feature allows you to provide composition instructions in a declarative way using `|` (pipe) or `,` (comma) delimiters when setting the `spring.cloud.function.definition` property. -For example +For example: ---- --spring.cloud.function.definition=uppercase|reverse ---- -Here we effectively provided a definition of a single function which itself is a composition of -function `uppercase` and function `reverse`. In fact that is one of the reasons why the property name is _definition_ and not _name_, -since the definition of a function can be a composition of several named functions. -And as mentioned you can use `,` instead of pipe (such as `...definition=uppercase,reverse`). + +Here, we effectively provided a definition of a single function which itself is a composition of function `uppercase` and function `reverse`. +In fact, that is one of the reasons why the property name is _definition_ and not _name_, since the definition of a function can be a composition of several named functions. +As mentioned, you can use `,` instead of `|`, such as `...definition=uppercase,reverse`. [[composing-non-functions]] === Composing non-Functions -Spring Cloud Function also supports composing Supplier with `Consumer` or `Function` as well as `Function` with `Consumer`. -What's important here is to understand the end product of such definitions. -Composing Supplier with Function still results in Supplier while composing Supplier with Consumer will effectively render Runnable. -Following the same logic composing Function with Consumer will result in Consumer. -And of course you can't compose uncomposable such as Consumer and Function, Consumer and Supplier etc. +Spring Cloud Function also supports composing `Supplier` with `Consumer` or `Function` as well as `Function` with `Consumer`. +What's important to understand is the end product of such definitions. +Composing `Supplier` with `Function` still results in `Supplier` while composing `Supplier` with `Consumer` will effectively render `Runnable`. +Following the same logic, composing `Function` with `Consumer` will result in `Consumer`. + +And, of course, you can't compose uncomposable objects such as `Consumer` and `Function`, `Consumer` and `Supplier`, etc. [[function-routing-and-filtering]] == Function Routing and Filtering -Since version 2.2 Spring Cloud Function provides routing feature allowing -you to invoke a single function which acts as a router to an actual function you wish to invoke -This feature is very useful in certain FAAS environments where maintaining configurations -for several functions could be cumbersome or exposing more than one function is not possible. +Since version 2.2, Spring Cloud Function provides a routing feature allowing you to invoke a single function, which acts as a router to an actual function you wish to invoke. +This feature is very useful in certain FAAS environments where maintaining configurations for several functions could be cumbersome or exposing more than one function is not possible. -The `RoutingFunction` is registered in _FunctionCatalog_ under the name `functionRouter`. For simplicity -and consistency you can also refer to `RoutingFunction.FUNCTION_NAME` constant. +The `RoutingFunction` is registered in _FunctionCatalog_ under the name `functionRouter`. +For simplicity and consistency, you can also refer to the `RoutingFunction.FUNCTION_NAME` constant. This function has the following signature: [source, java] ---- 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 + +The routing instructions could be communicated in several ways. +We support providing instructions via Message headers, System properties as well as a pluggable strategy. +Let's look at some of the details. [[messageroutingcallback]] === MessageRoutingCallback @@ -195,12 +230,13 @@ The `MessageRoutingCallback` is a strategy to assist with determining the name o [source, java] ---- public interface MessageRoutingCallback { - FunctionRoutingResult routingResult(Message message); - . . . + default String routingResult(Message message) { + return (String) message.getHeaders().get(FunctionProperties.FUNCTION_DEFINITION); + } } ---- -All you need to do is implement and register it as a bean to be picked up by the `RoutingFunction`. +All you need to do is implement and register a `MessageRoutingCallback` as a bean to be picked up by the `RoutingFunction`. For example: [source, java] @@ -216,64 +252,59 @@ public MessageRoutingCallback customRouter() { } ---- -In the preceding example you can see a very simple implementation of `MessageRoutingCallback` which determines the function definition from -`FunctionProperties.FUNCTION_DEFINITION` Message header of the incoming Message and returns the instance of `String` representing the definition of function to invoke. +In the preceding example you can see a very simple implementation of `MessageRoutingCallback`, which determines the function definition from the `FunctionProperties.FUNCTION_DEFINITION` `Message` header of the incoming `Message`, returning an instance of `String` representing the definition of the function to invoke. *Message Headers* -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 and provide SpEL expression that should resolve -into definition of a function (as described above). +If the input argument is of type `Message`, you can communicate routing instructions 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 the _Spring Expression Language_ (SpEL). +For more static cases you can use the `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 the `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`). +NOTE: SpEL evaluation context's root object is the actual input argument, so in the case of `Message` you can construct an 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. +IMPORTANT: SpEL allows users to provide a String representation of the Java code to be executed. +Given that the `spring.cloud.function.routing-expression` could be provided via Message headers means that the ability to set such expressions could be exposed to the end user (i.e. HTTP Headers when using the 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 is designed to only evaluate the context object (Message in our case). +On the other hand, all expressions that are set via property or system environment variable are evaluated against `StandardEvaluationContext` allowing for the full flexibility of the Java language. +While setting expressions 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 other Spring project, a third party, or a custom implementation created by the end user. +Such endpoints must be secured using industry standard web security practices. +Spring Cloud Function does not expose any 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 -header and the framework will propagate it as well as other HTTP headers as Message headers. +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 header and the framework will propagate it, along with other HTTP headers, as Message headers. *Application Properties* -Routing instruction can also be communicated via `spring.cloud.function.definition` -or `spring.cloud.function.routing-expression` as application properties. The rules described in the -previous section apply here as well. The only difference is you provide these instructions as -application properties (e.g., `--spring.cloud.function.definition=foo`). +Routing instructions can also be communicated via `spring.cloud.function.definition` or `spring.cloud.function.routing-expression` as application properties. +The rules described in the previous section apply here as well. The only difference is you provide these instructions as application properties (e.g., `--spring.cloud.function.definition=foo`). -NOTE: It is important to understand that providing `spring.cloud.function.definition` -or `spring.cloud.function.routing-expression` as Message headers will only work for imperative functions (e.g., `Function`). -That is to say that we can _only_ route ***per-message*** with imperative functions. With reactive functions we can not route -***per-message***. Therefore you can only provide your routing instructions as Application Properties. -It's all about unit-of-work. In imperative function unit of work is Message so we can route based on such unit-of-work. -With reactive function unit-of-work is the entire stream, so we'll act only on the instruction provided via application -properties and route the entire stream. +NOTE: It is important to understand that providing `spring.cloud.function.definition` or `spring.cloud.function.routing-expression` as Message headers will only work for imperative functions (e.g. `Function`). +That is to say that we can _only_ route ***per-message*** with imperative functions. +With reactive functions we can not route ***per-message***. +Therefore, you can only provide your routing instructions as application properties. +It's all about unit-of-work. +In an imperative function, the unit of work is Message so we can route based on such unit-of-work. +With a reactive function, the unit of work is the entire stream, so we'll act only on the instruction provided via application properties and route the entire stream. *Order of priority for routing instructions* -Given that we have several mechanisms of providing routing instructions it is important to understand the priorities for -conflict resolutions in the event multiple mechanisms are used at the same time, so here is the order: +Given that we have several mechanisms of providing routing instructions, it is important to understand the priorities for conflict resolution in the event multiple mechanisms are used at the same time. +Here is the order: -1. `MessageRoutingCallback` (If function is imperative will take over regardless if anything else is defined) +1. `MessageRoutingCallback` (Takes precedence when function is imperative regardless if anything else is defined) 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. +In the event a route-to function is not available in the 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. +There are cases when such behavior is not desired and you may want to have some "catch-all" type function capable of handling such messages. +To accomplish that, the framework provides the `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. +If you need something more sophisticated, all you need to do is provide your own implementation of this strategy and register it as a bean. [source, java] ---- @@ -290,44 +321,48 @@ public DefaultMessageRoutingHandler defaultRoutingHandler() { [[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. -For example, you may want to log it, or you may want to maintain the counter of discarded messages. you may also want to do nothing at all. + +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 interpretations of what it could mean in the context of your application. +For example, you may want to log it, or you may want to maintain a counter of discarded messages. +You may also want to do nothing at all. + Because of these different paths, we do not provide a general configuration option for how to deal with discarded messages. -Instead we simply recommend to define a simple Consumer which would signify the 'discard' path: +Instead, we simply recommend to define a simple `Consumer` which would signify the 'discard' path: [source, java] ---- @Bean public Consumer devNull() { - // log, count or whatever + // log, count, or whatever } ---- -Now you can have routing expression that really only has two paths effectively becoming a filter. For example: + +Now you can have a routing expression that really only has two paths effectively becoming a filter. +For example: [source, text] ---- --spring.cloud.function.routing-expression=headers.contentType.toString().equals('text/plain') ? 'echo' : 'devNull' ---- -Every message that does not fit criteria to go to 'echo' function will go to 'devNull' where you can simply do nothing with it. -The signature `Consumer` will also ensure that no type conversion will be attempted resulting in almost no execution overhead. +Every message that does not fit the criteria to go to 'echo' function will go to 'devNull' where you can simply do nothing with it. +The signature `Consumer` will also ensure that no type conversion will be attempted resulting in almost no execution overhead. -IMPORTANT: When dealing with reactive inputs (e.g., Publisher), routing instructions must only be provided via Function properties. This is -due to the nature of the reactive functions which are invoked only once to pass a Publisher and the rest -is handled by the reactor, hence we can not access and/or rely on the routing instructions communicated via individual -values (e.g., Message). +IMPORTANT: When dealing with reactive inputs (e.g. Publisher), routing instructions must only be provided via Function properties. +This is due to the nature of the reactive functions which are invoked only once to pass a `Publisher` and the rest is handled by the reactor, hence we cannot access and/or rely on the routing instructions communicated via individual values (e.g., Message). [[multiple-routers]] === 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 than one routing function. +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 than 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. +You can pass `spring.cloud.function.routing-expression` or `spring.cloud.function.definition` to `RoutingFunction` as key/value pairs in the map. -Here is a simple example +Here is a simple example: ---- @Configuration @@ -352,9 +387,9 @@ protected static class MultipleRouterConfiguration { } ---- -and a test that demonstrates how it works +Here is a test to demonstrates how it works: -` +[source, java] ---- @Test public void testMultipleRouters() { @@ -372,15 +407,17 @@ public void testMultipleRouters() { } ---- -[[input/output-enrichment]] +[[input-output-enrichment]] == 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. +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. -You can always accomplish it via <>. Such approach provides several benefits: +You can always accomplish it via <>. +Such an approach provides several benefits: -- It allows you to isolate this non-functional concern into a separate function which you can compose with the business function as function definition. -- It provides you with complete freedom (and danger) as to what you can modify before incoming message reaches the actual business function. +- It allows you to isolate this non-functional concern into a separate function which you can compose with the business function as a function definition. +- It provides you with complete freedom (and danger) as to what you can modify before the incoming message reaches the actual business function. [source, java] ---- @@ -395,16 +432,16 @@ public Function, Message> myBusinessFunction() { } ---- -And then compose your function by providing the following function definition `enrich|myBusinessFunction`. +Then, compose your function by providing the following function definition: `enrich|myBusinessFunction`. -While the described approach is the most flexible, it is also the most involved as it requires you to write some code, make it a bean or -manually register it as a function before you can compose it with the business function as you can see from the preceding example. +While the described approach is the most flexible, it is also the most involved. +It requires you to write some code, then make it a bean, or manually register it as a function before you can compose it with the business function as you can see from the preceding example. -But what if modifications (enrichments) you are trying to make are trivial as they are in the preceding example? Is there a simpler and more dynamic and configurable - mechanism to accomplish the same? +But what if modifications (enrichments) you are trying to make are trivial as they are in the preceding example? +Is there a simpler and more dynamic and configurable mechanism to accomplish the same? -Since version 3.1.3, the framework allows you to provide SpEL expression to enrich individual message headers for both input going into function and -and output coming out of it. Let’s look at one of the tests as the example. +Since version 3.1.3, the framework allows you to provide SpEL expression to enrich individual message headers for both input going into a function and and output coming out of it. +Let’s look at one of the tests as an example. [source, java] ---- @@ -422,30 +459,33 @@ public void testMixedInputOutputHeaderMapping() throws Exception { FunctionCatalog functionCatalog = context.getBean(FunctionCatalog.class); FunctionInvocationWrapper function = functionCatalog.lookup("split"); - Message result = (Message) function.apply(MessageBuilder.withPayload("helo") + Message result = (Message) function.apply(MessageBuilder.withPayload("hello") .setHeader(MessageHeaders.CONTENT_TYPE, "application/json") - .setHeader("path", "foo/bar/baz").build()); - assertThat(result.getHeaders().containsKey("keyOut1")).isTrue(); + .setHeader("path", "foo/bar/baz") + .build()); + assertThat(result.getHeaders()).containsKey("keyOut1")); assertThat(result.getHeaders().get("keyOut1")).isEqualTo("hello1"); - assertThat(result.getHeaders().containsKey("keyOut2")).isTrue(); + assertThat(result.getHeaders()).containsKey("keyOut2")); assertThat(result.getHeaders().get("keyOut2")).isEqualTo("application/json"); } } ---- -Here you see a properties called `input-header-mapping-expression` and `output-header-mapping-expression` preceded by the name of the function (i.e., `split`) and followed by the name of the message header key you want to set and the value as SpEL expression. The first expression (for 'keyOut1') is literal SpEL expressions enclosed in single quotes, effectively setting 'keyOut1' to value `hello1`. The `keyOut2` is set to the value of existing 'contentType' header. +Here you see properties called `input-header-mapping-expression` and `output-header-mapping-expression` preceded by the name of the function (i.e. `split`) followed by the name of the message header key you want to set and the value as SpEL expression. +The first expression (for 'keyOut1') is a literal SpEL expressions enclosed in single quotes, effectively setting 'keyOut1' to value `hello1`. +The `keyOut2` is set to the value of the existing 'contentType' header. -You can also observe some interesting features in the input header mapping where we actually splitting a value of the existing header 'path', setting individual values of key1 and key2 to the values of split elements based on the index. +You can also observe some interesting features in the input header mapping where we are actually splitting a value of the existing header 'path', setting individual values of key1 and key2 to the values of split elements based on the index. -NOTE: if for whatever reason the provided expression evaluation fails, the execution of the function will proceed as if nothing ever happen. -However you will see the WARN message in your logs informing you about it +NOTE: If for whatever reason the provided expression evaluation fails, the execution of the function will proceed as if nothing ever happened. +However, you will see the WARN message in your logs informing you about it. [source, text] ---- o.s.c.f.context.catalog.InputEnricher : Failed while evaluating expression "hello1" on incoming message. . . ---- -In the event you are dealing with functions that have multiple inputs (next section), you can use index immediately after `input-header-mapping-expression` +In the event you are dealing with functions that have multiple inputs (next section), you can use an index immediately after `input-header-mapping-expression`: [source, text] ---- @@ -456,14 +496,13 @@ In the event you are dealing with functions that have multiple inputs (next sect [[function-arity]] == Function Arity -There are times when a stream of data needs to be categorized and organized. For example, -consider a classic big-data use case of dealing with unorganized data containing, let’s say, -‘orders’ and ‘invoices’, and you want each to go into a separate data store. -This is where function arity (functions with multiple inputs and outputs) support -comes to play. +There are times when a stream of data needs to be categorized and organized. +For example, consider a classic big-data use case of dealing with unorganized data containing, let’s say, ‘orders’ and ‘invoices’, and you want each to go into a separate data store. +This is where function arity (functions with multiple inputs and outputs) support comes to play. + +Let’s look at an example of such a function.MessageRoutingCallback -Let’s look at an example of such a function (full implementation details are available -https://github.com/spring-cloud/spring-cloud-function/blob/main/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryMultiInOutTests.java[here]), +NOTE: Full implementation details are available https://github.com/spring-cloud/spring-cloud-function/blob/main/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryMultiInOutTests.java[here]. [source, java] ---- @@ -475,19 +514,12 @@ public Function, Tuple2, Flux>> organise() { Given that Project Reactor is a core dependency of SCF, we are using its Tuple library. Tuples give us a unique advantage by communicating to us both _cardinality_ and _type_ information. -Both are extremely important in the context of SCSt. Cardinality lets us know -how many input and output bindings need to be created and bound to the corresponding -inputs and outputs of a function. Awareness of the type information ensures proper type -conversion. +Both are extremely important in the context of SCSt. Cardinality lets us know how many input and output bindings need to be created and bound to the corresponding inputs and outputs of a function. +Awareness of the type information ensures proper type conversion. -Also, this is where the ‘index’ part of the naming convention for binding -names comes into play, since, in this function, the two output binding -names are `organise-out-0` and `organise-out-1`. +Also, this is where the ‘index’ part of the naming convention for binding names comes into play, since, in this function, the two output binding names are `organise-out-0` and `organise-out-1`. -IMPORTANT: IMPORTANT: At the moment, function arity is *only* supported for reactive functions -(`Function...>, TupleN...>>`) centered on Complex event processing -where evaluation and computation on confluence of events typically requires view into a -stream of events rather than single event. +IMPORTANT: At the moment, function arity is *only* supported for reactive functions (`Function...>, TupleN...>>`) centered on complex event processing where evaluation and computation on confluence of events typically requires view into a stream of events rather than single event. [[input-header-propagation]] == Input Header propagation @@ -495,7 +527,9 @@ stream of events rather than single event. 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. +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`. @@ -519,6 +553,7 @@ As you know you can still invoke this function by sending a Message to it (frame By simply setting `spring.cloud.function.configuration.uppercase.copy-input-headers` to `true`, the following assertion will be true as well +[source, java] ---- Function, Message> uppercase = catalog.lookup("uppercase", "application/json"); Message result = uppercase.apply(MessageBuilder.withPayload("bob").setHeader("foo", "bar").build()); @@ -540,10 +575,9 @@ using the following function as an example: public Function personFunction {..} ---- -The function shown in the preceding example expects a `Person` object as an argument and produces a String type as an output. If such function is -invoked with the type `Person`, than all works fine. But typically function plays a role of a handler for the incoming data which most often comes -in the raw format such as `byte[]`, `JSON String` etc. In order for the framework to succeed in passing the incoming data as an argument to -this function, it has to somehow transform the incoming data to a `Person` type. +The function shown in the preceding example expects a `Person` object as an argument and produces a String type as an output. If such function is invoked with the type `Person`, then all works fine. +But, typically function plays a role of a handler for the incoming data which most often comes in the raw format such as `byte[]`, `JSON String` etc. +In order for the framework to succeed in passing the incoming data as an argument to this function, it has to somehow transform the incoming data to a `Person` type. Spring Cloud Function relies on two native to Spring mechanisms to accomplish that. @@ -552,31 +586,28 @@ Spring Cloud Function relies on two native to Spring mechanisms to accomplish th This means that depending on the type of the raw data (Message or non-Message) Spring Cloud Function will apply one or the other mechanisms. -For most cases when dealing with functions that are invoked as part of some other request (e.g., HTTP, Messaging etc) the framework relies on `MessageConverters`, -since such requests already converted to Spring `Message`. In other words, the framework locates and applies the appropriate `MessageConverter`. -To accomplish that, the framework needs some instructions from the user. One of these instructions is already provided by the signature of the function -itself (Person type). Consequently, in theory, that should be (and, in some cases, is) enough. However, for the majority of use cases, in order to -select the appropriate `MessageConverter`, the framework needs an additional piece of information. That missing piece is `contentType` header. +For most cases when dealing with functions that are invoked as part of some other request (e.g., HTTP, Messaging etc) the framework relies on `MessageConverters`, since such requests already converted to Spring `Message`. +In other words, the framework locates and applies the appropriate `MessageConverter`. +To accomplish that, the framework needs some instructions from the user. +One of these instructions is already provided by the signature of the function itself (Person type). +Consequently, in theory, that should be (and, in some cases, is) enough. +However, for the majority of use cases, in order to select the appropriate `MessageConverter`, the framework needs an additional piece of information. +That missing piece is `contentType` header. Such header usually comes as part of the Message where it is injected by the corresponding adapter that created such Message in the first place. For example, HTTP POST request will have its content-type HTTP header copied to `contentType` header of the Message. For cases when such header does not exist framework relies on the default content type as `application/json`. - [[content-type-versus-argument-type]] === Content Type versus Argument Type As mentioned earlier, for the framework to select the appropriate `MessageConverter`, it requires argument type and, optionally, content type information. -The logic for selecting the appropriate `MessageConverter` resides with the argument resolvers which trigger right before the invocation of the user-defined -function (which is when the actual argument type is known to the framework). -If the argument type does not match the type of the current payload, the framework delegates to the stack of the -pre-configured `MessageConverters` to see if any one of them can convert the payload. +The logic for selecting the appropriate `MessageConverter` resides with the argument resolvers which trigger right before the invocation of the user-defined function (which is when the actual argument type is known to the framework). +If the argument type does not match the type of the current payload, the framework delegates to the stack of the pre-configured `MessageConverters` to see if any one of them can convert the payload. -The combination of `contentType` and argument type is the mechanism by which framework determines if message can be converted to a target type by locating -the appropriate `MessageConverter`. -If no appropriate `MessageConverter` is found, an exception is thrown, which you can handle by adding a custom `MessageConverter` -(see `xref:spring-cloud-function/programming-model.adoc#user-defined-message-converters[User-defined Message Converters]`). +The combination of `contentType` and argument type is the mechanism by which framework determines if message can be converted to a target type by locating the appropriate `MessageConverter`. +If no appropriate `MessageConverter` is found, an exception is thrown, which you can handle by adding a custom `MessageConverter` (see `xref:spring-cloud-function/programming-model.adoc#user-defined-message-converters[User-defined Message Converters]`). NOTE: Do not expect `Message` to be converted into some other type based only on the `contentType`. Remember that the `contentType` is complementary to the target type. @@ -597,8 +628,7 @@ Message toMessage(Object payload, @Nullable MessageHeaders headers); It is important to understand the contract of these methods and their usage, specifically in the context of Spring Cloud Stream. The `fromMessage` method converts an incoming `Message` to an argument type. -The payload of the `Message` could be any type, and it is -up to the actual implementation of the `MessageConverter` to support multiple types. +The payload of the `Message` could be any type, and it is up to the actual implementation of the `MessageConverter` to support multiple types. [[provided-messageconverters]] @@ -611,13 +641,12 @@ The following list describes the provided `MessageConverters`, in order of prece . `ByteArrayMessageConverter`: Supports conversion of the payload of the `Message` from `byte[]` to `byte[]` for cases when `contentType` is `application/octet-stream`. It is essentially a pass through and exists primarily for backward compatibility. . `StringMessageConverter`: Supports conversion of any type to a `String` when `contentType` is `text/plain`. -When no appropriate converter is found, the framework throws an exception. When that happens, you should check your code and configuration and ensure you did -not miss anything (that is, ensure that you provided a `contentType` by using a binding or a header). -However, most likely, you found some uncommon case (such as a custom `contentType` perhaps) and the current stack of provided `MessageConverters` -does not know how to convert. If that is the case, you can add custom `MessageConverter`. See xref:spring-cloud-function/programming-model.adoc#user-defined-message-converters[User-defined Message Converters]. +When no appropriate converter is found, the framework throws an exception. When that happens, you should check your code and configuration and ensure you did not miss anything (that is, ensure that you provided a `contentType` by using a binding or a header). +However, most likely, you found some uncommon case (such as a custom `contentType` perhaps) and the current stack of provided `MessageConverters` does not know how to convert. +If that is the case, you can add custom `MessageConverter`. See xref:spring-cloud-function/programming-model.adoc#user-defined-message-converters[User-defined Message Converters]. [[user-defined-message-converters]] -=== User-defined Message Converters +=== User-defined MessageConverters Spring Cloud Function exposes a mechanism to define and register additional `MessageConverters`. To use it, implement `org.springframework.messaging.converter.MessageConverter`, configure it as a `@Bean`. @@ -664,17 +693,14 @@ public class MyCustomMessageConverter extends AbstractMessageConverter { === Note on JSON options In Spring Cloud Function we support Jackson and Gson mechanisms to deal with JSON. -And for your benefit have abstracted it under `org.springframework.cloud.function.json.JsonMapper` which itself is aware of two mechanisms and will use the one selected -by you or following the default rule. +And for your benefit have abstracted it under `org.springframework.cloud.function.json.JsonMapper` which itself is aware of two mechanisms and will use the one selected by you or following the default rule. The default rules are as follows: * Whichever library is on the classpath that is the mechanism that is going to be used. So if you have `com.fasterxml.jackson.*` to the classpath, Jackson is going to be used and if you have `com.google.code.gson`, then Gson will be used. * If you have both, then Gson will be the default, or you can set `spring.cloud.function.preferred-json-mapper` property with either of two values: `gson` or `jackson`. - -That said, the type conversion is usually transparent to the developer, however given that `org.springframework.cloud.function.json.JsonMapper` is also registered as a bean -you can easily inject it into your code if needed. - +That said, the type conversion is usually transparent to the developer. +However, given that `org.springframework.cloud.function.json.JsonMapper` is also registered as a bean you can easily inject it into your code if needed. [[kotlin-lambda-support]] == Kotlin Lambda support @@ -700,18 +726,47 @@ open fun kotlinConsumer(): (String) -> Unit { } ---- -The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of -`Supplier`, `Function` and `Consumer`, and thus supported/recognized signatures by the framework. -While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the -same rules for signature transformation outlined in "Java 8 function support" section are applied here as well. +The above represents Kotlin lambdas configured as Spring beans. The signature of each maps to a Java equivalent of `Supplier`, `Function` and `Consumer`, and thus supported/recognized signatures by the framework. +While mechanics of Kotlin-to-Java mapping are outside of the scope of this documentation, it is important to understand that the same rules for signature transformation outlined in "Java 8 function support" section are applied here as well. -To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate -autoconfiguration and supporting classes. +To enable Kotlin support all you need is to add Kotlin SDK libraries on the classpath which will trigger appropriate autoconfiguration and supporting classes. [[function-component-scan]] == Function Component Scan -Spring Cloud Function will scan for implementations of `Function`, `Consumer` and `Supplier` in a package called `functions` if it exists. Using this -feature you can write functions that have no dependencies on Spring - not even the `@Component` annotation is needed. If you want to use a different -package, you can set `spring.cloud.function.scan.packages`. You can also use `spring.cloud.function.scan.enabled=false` to switch off the scan completely. +Spring Cloud Function will scan for implementations of `Function`, `Consumer` and `Supplier` in a package called `functions` if it exists. +Using this feature you can write functions that have no dependencies on Spring - not even the `@Component` annotation is needed. +If you want to use a different package, you can set `spring.cloud.function.scan.packages`. +You can also use `spring.cloud.function.scan.enabled=false` to switch off the scan completely. + +[[data-masking]] +== Data Masking + +A typical application comes with several levels of logging. +Certain cloud/serverless platforms may include sensitive data in the packets that are being logged for everyone to see. +While it is the responsibility of individual developers to inspect the data that is being logged, since logging comes from the framework itself, as of version 4.1, we have introduced `JsonMasker` to initially help with masking sensitive data in AWS Lambda payloads. +However, the `JsonMasker` is generic and is available to any module. +At the moment it will only work with structured data such as JSON. +All you need is to specify the keys you want to mask and it will take care of the rest. +Keys should be specified in the file `META-INF/mask.keys`. +The format of the file is very simple where you can delimit several keys by commas, new line, or both. + +Here is the example of the contents of such file: + +[source, text] +---- +eventSourceARN +asdf1, SS +---- + +Here you see three keys defined. +Once such a file exists, the `JsonMasker` will use it to mask values of the keys specified. +And, here is the sample code that shows the usage: + +[source, java] +---- +private final static JsonMasker masker = JsonMasker.INSTANCE(); +// . . . +logger.info("Received: " + masker.mask(new String(payload, StandardCharsets.UTF_8))); +---- diff --git a/docs/modules/ROOT/pages/spring-cloud-function/standalone-web-applications.adoc b/docs/modules/ROOT/pages/spring-cloud-function/standalone-web-applications.adoc index d80f6bcb4..374290089 100644 --- a/docs/modules/ROOT/pages/spring-cloud-function/standalone-web-applications.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-function/standalone-web-applications.adoc @@ -46,7 +46,7 @@ See <> to see the details and example on how to 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`) +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. @@ -82,7 +82,7 @@ of the actual URL, giving user ability to use it for evaluation and computation. In situations where there are more than one function in catalog there may be a need to only export certain functions or function compositions. In that case you can use the same `spring.cloud.function.definition` property listing functions you intend to export delimited by `;`. -Note that in this case nothing will be mapped to the root path and functions that are not listed (including compositions) are not going to be exported +Note that in this case nothing will be mapped to the root path and functions that are not listed (including compositions) are not going to be exported. For example, @@ -101,7 +101,7 @@ This will only export function composition `foo|bar` and function `baz` regardle == Http Headers propagation By default most request `HttpHeaders` are copied into the response `HttpHeaders`. If you require to filter out certain headers you can provide the names of those headers using -`spring.cloud.function.http.ignored-headers` delimited by comas. For example, `spring.cloud.function.http.ignored-headers=foo,bar` +`spring.cloud.function.http.ignored-headers` delimited by comas. For example, `spring.cloud.function.http.ignored-headers=foo,bar`. [[crud-rest-with-spring-cloud-function]] == CRUD REST with Spring Cloud Function diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..96a72aebd --- /dev/null +++ b/docs/package.json @@ -0,0 +1,10 @@ +{ + "dependencies": { + "antora": "3.2.0-alpha.4", + "@antora/atlas-extension": "1.0.0-alpha.2", + "@antora/collector-extension": "1.0.0-alpha.3", + "@asciidoctor/tabs": "1.0.0-beta.6", + "@springio/antora-extensions": "1.11.1", + "@springio/asciidoctor-extensions": "1.0.0-alpha.10" + } + } diff --git a/docs/pom.xml b/docs/pom.xml index 3b6c99f8f..f2c7245d0 100644 --- a/docs/pom.xml +++ b/docs/pom.xml @@ -8,7 +8,7 @@ org.springframework.cloud spring-cloud-function-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT jar Spring Cloud Function Docs @@ -51,8 +51,13 @@ antora-component-version-maven-plugin - io.spring.maven.antora + org.antora antora-maven-plugin + + + + + org.apache.maven.plugins diff --git a/docs/src/main/asciidoc/README.adoc b/docs/src/main/asciidoc/README.adoc index 5690a613f..bfee75f35 100644 --- a/docs/src/main/asciidoc/README.adoc +++ b/docs/src/main/asciidoc/README.adoc @@ -16,7 +16,7 @@ image::https://travis-ci.org/spring-cloud/spring-cloud-function.svg?branch={bran = Building :page-section-summary-toc: 1 -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/partials/contributing.adoc[] +include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/modules/ROOT/partials/building.adoc[] [[contributing]] = Contributing diff --git a/pom.xml b/pom.xml index 50878f151..23e50338e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,13 +6,13 @@ spring-cloud-function-parent Spring Cloud Function Parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT pom org.springframework.cloud spring-cloud-build - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT @@ -20,7 +20,7 @@ 17 ${java.version} ${java.version} - 1.0.27.RELEASE + 1.0.31.RELEASE spring-cloud-function true true @@ -41,7 +41,6 @@ - @@ -57,7 +56,6 @@ - org.codehaus.mojo @@ -162,7 +160,6 @@ spring-cloud-function-deployer spring-cloud-function-adapters spring-cloud-function-integration - spring-cloud-function-rsocket spring-cloud-function-kotlin docs diff --git a/spring-cloud-function-adapters/pom.xml b/spring-cloud-function-adapters/pom.xml index 1fb4c8dd8..8c0116b93 100644 --- a/spring-cloud-function-adapters/pom.xml +++ b/spring-cloud-function-adapters/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-function-parent - 4.1.2-SNAPSHOT + 4.3.0-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 13c4f2f2e..e38cc179a 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/pom.xml @@ -13,13 +13,13 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT UTF-8 UTF-8 - 3.11.4 + 3.14.0 1.12.29 1.0.1 1.1.5 diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSLambdaUtils.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSLambdaUtils.java index 916ff31a0..0fb568dfd 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 @@ -34,6 +34,7 @@ import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.cloud.function.utils.JsonMasker; import org.springframework.http.HttpStatus; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -67,6 +68,8 @@ public final class AWSLambdaUtils { */ public static final String AWS_CONTEXT = "aws-context"; + private final static JsonMasker masker = JsonMasker.INSTANCE(); + private AWSLambdaUtils() { } @@ -102,11 +105,15 @@ public static Message generateMessage(byte[] payload, Type inputType, bo return generateMessage(payload, inputType, isSupplier, jsonMapper, null); } + private static String mask(String value) { + return masker.mask(value); + } + @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)); + logger.info("Received: " + mask(new String(payload, StandardCharsets.UTF_8))); } Object structMessage = jsonMapper.fromJson(payload, Object.class); @@ -119,7 +126,7 @@ public static Message generateMessage(byte[] payload, Type inputType, bo MessageBuilder builder = MessageBuilder .withPayload(structMessage instanceof Map msg && msg.containsKey("payload") - ? ((String) msg.get("payload")).getBytes(StandardCharsets.UTF_8) + ? (msg.get("payload")) : payload); if (isApiGateway) { builder.setHeader(AWSLambdaUtils.AWS_API_GATEWAY, true); 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 f90f80dee..7077a1595 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSTypesMessageConverter.java @@ -17,11 +17,14 @@ package org.springframework.cloud.function.adapter.aws; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.nio.charset.StandardCharsets; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer; import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers; +import com.amazonaws.services.lambda.runtime.serialization.events.serializers.S3EventSerializer; import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils; import org.springframework.cloud.function.context.config.JsonMessageConverter; @@ -30,6 +33,7 @@ import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.MessageConverter; +import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; /** @@ -44,6 +48,9 @@ class AWSTypesMessageConverter extends JsonMessageConverter { private final JsonMapper jsonMapper; + @SuppressWarnings("rawtypes") + private final AtomicReference s3EventSerializer = new AtomicReference<>(); + AWSTypesMessageConverter(JsonMapper jsonMapper) { this(jsonMapper, new MimeType("application", "json"), new MimeType(CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getType(), CloudEventMessageUtils.APPLICATION_CLOUDEVENTS.getSubtype() + "+json")); @@ -75,7 +82,6 @@ protected Object convertFromInternal(Message message, Class targetClass, @ if (message.getPayload().getClass().isAssignableFrom(targetClass)) { return message.getPayload(); } - if (targetClass.getPackage() != null && targetClass.getPackage().getName().startsWith("com.amazonaws.services.lambda.runtime.events")) { PojoSerializer serializer = LambdaEventSerializers.serializerFor(targetClass, Thread.currentThread().getContextClassLoader()); @@ -89,11 +95,11 @@ protected Object convertFromInternal(Message message, Class targetClass, @ } else { Object body; - if (message.getHeaders().containsKey("payload")) { - body = message.getPayload(); + if (structMessage.containsKey("body")) { + body = structMessage.get("body"); } else { - body = structMessage.get("body"); + body = message.getPayload(); } Object convertedResult = this.jsonMapper.fromJson(body, targetClass); return convertedResult; @@ -110,12 +116,23 @@ protected boolean canConvertTo(Object payload, @Nullable MessageHeaders headers) } + @SuppressWarnings("unchecked") @Override protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { if (payload instanceof String && headers.containsKey(AWSLambdaUtils.IS_BASE64_ENCODED) && (boolean) headers.get(AWSLambdaUtils.IS_BASE64_ENCODED)) { return ((String) payload).getBytes(StandardCharsets.UTF_8); } + if (payload.getClass().getName().equals("com.amazonaws.services.lambda.runtime.events.S3Event")) { + if (this.s3EventSerializer.get() == null) { + this.s3EventSerializer.set(new S3EventSerializer<>().withClassLoader(ClassUtils.getDefaultClassLoader())); + } + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + this.s3EventSerializer.get().toJson(payload, stream); + return stream.toByteArray(); + } + + return jsonMapper.toJson(payload); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java index 917ca8693..0b55bc283 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoop.java @@ -28,10 +28,14 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.LambdaRuntime; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.context.config.RoutingFunction; @@ -130,6 +134,8 @@ private void eventLoop(ConfigurableApplicationContext context) { logger.debug("Attempting to get new event"); ResponseEntity response = this.pollForData(rest, requestEntity); + Context clientContext = generateClientContext(response.getHeaders()); + if (logger.isDebugEnabled()) { logger.debug("New Event received: " + response); } @@ -140,9 +146,9 @@ private void eventLoop(ConfigurableApplicationContext context) { FunctionInvocationWrapper function = locateFunction(environment, functionCatalog, response.getHeaders()); ByteArrayInputStream is = new ByteArrayInputStream(response.getBody().getBytes(StandardCharsets.UTF_8)); - Message requestMessage = AWSLambdaUtils.generateMessage(is, function.getInputType(), function.isSupplier(), mapper, null); - + Message requestMessage = AWSLambdaUtils.generateMessage(is, function.getInputType(), function.isSupplier(), mapper, clientContext); Object functionResponse = function.apply(requestMessage); + byte[] responseBytes = AWSLambdaUtils.generateOutputFromObject(requestMessage, functionResponse, mapper, function.getOutputType()); String invocationUrl = MessageFormat @@ -157,12 +163,91 @@ private void eventLoop(ConfigurableApplicationContext context) { } } catch (Exception e) { + e.printStackTrace(); this.propagateAwsError(requestId, e, mapper, runtimeApi, rest); } } } } + private Context generateClientContext(HttpHeaders headers) { + + Map environment = System.getenv(); + + Context context = new Context() { + + @Override + public int getRemainingTimeInMillis() { + long now = System.currentTimeMillis(); + if (!headers.containsKey("Lambda-Runtime-Deadline-Ms")) { + return 0; + } + int delta = (int) (Long.parseLong(headers.getFirst("Lambda-Runtime-Deadline-Ms")) - now); + return delta > 0 ? delta : 0; + } + + @Override + public int getMemoryLimitInMB() { + if (!environment.containsKey("AWS_LAMBDA_FUNCTION_MEMORY_SIZE")) { + return 128; + } + return Integer.parseInt(environment.getOrDefault("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "128")); + } + + @Override + public LambdaLogger getLogger() { + return LambdaRuntime.getLogger(); + } + + @Override + public String getLogStreamName() { + return environment.get("LOG_STREAM_NAME"); + } + + @Override + public String getLogGroupName() { + return environment.get("LOG_GROUP_NAME"); + } + + @Override + public String getInvokedFunctionArn() { + return headers.getFirst("Lambda-Runtime-Invoked-Function-Arn"); + } + + @Override + public CognitoIdentity getIdentity() { + return null; + } + + @Override + public String getFunctionVersion() { + return environment.get("FUNCTION_VERSION"); + } + + @Override + public String getFunctionName() { + return environment.get("FUNCTION_NAME"); + } + + @Override + public ClientContext getClientContext() { + return null; + } + + @Override + public String getAwsRequestId() { + return headers.getFirst("Lambda-Runtime-Aws-Request-Id"); + } + + public String toString() { + return "FUNCTION NAME: " + getFunctionName() + ", FUNCTION VERSION: " + getFunctionVersion() + + ", FUNCTION ARN: " + getInvokedFunctionArn() + ", FUNCTION MEM LIMIT: " + getMemoryLimitInMB() + + ", FUNCTION DEADLINE: " + getRemainingTimeInMillis(); + } + }; + return context; + } + private void propagateAwsError(String requestId, Exception e, JsonMapper mapper, String runtimeApi, RestTemplate rest) { String errorMessage = e.getMessage(); String errorType = e.getClass().getSimpleName(); diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java index 93bfde19d..112183818 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/FunctionInvoker.java @@ -85,6 +85,9 @@ public void handleRequest(InputStream input, OutputStream output, Context contex if (!this.started) { this.start(); } + if (context == null) { + logger.warn("Lambda is invoked with null Context"); + } Message requestMessage = AWSLambdaUtils .generateMessage(input, this.function.getInputType(), this.function.isSupplier(), jsonMapper, context); @@ -126,7 +129,7 @@ private void start() { 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"); + + "or pass function definition as a constructor argument to this FunctionInvoker"); } Set names = functionCatalog.getNames(null); if (names.size() == 1) { @@ -135,7 +138,7 @@ private void start() { + "If invocation is over API Gateway, Message headers can be provided as HTTP headers."); } else { - logger.info("More then one function is available in FunctionCatalog. " + names + logger.info("More than one function is available in FunctionCatalog. " + names + " Will default to RoutingFunction, " + "Expecting 'spring.cloud.function.definition' or 'spring.cloud.function.routing-expression' as Message headers. " + "If invocation is over API Gateway, Message headers can be provided as HTTP headers."); diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/LambdaDestinationResolver.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/LambdaDestinationResolver.java index 61b1f8bf9..dcc19ea1c 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/LambdaDestinationResolver.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/LambdaDestinationResolver.java @@ -40,7 +40,7 @@ public class LambdaDestinationResolver implements DestinationResolver { @Override public String destination(Supplier supplier, String name, Object value) { if (logger.isDebugEnabled()) { - logger.debug("Lambda invoming value: " + value); + logger.debug("Lambda incoming value: " + value); } String destination = "unknown"; if (value instanceof Message) { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java index 868c2ba01..817cbd776 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/CustomRuntimeEventLoopTest.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.adapter.aws; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -219,7 +220,7 @@ public void test_definitionLookupAndComposition() throws Exception { protected static class SingleFunctionConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } } @@ -236,7 +237,7 @@ public Function, Flux> uppercase() { protected static class MultipleFunctionConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean @@ -246,7 +247,7 @@ public Function toPersonJson() { @Bean public Function uppercasePerson() { - return p -> new Person(p.getName().toUpperCase()); + return p -> new Person(p.getName().toUpperCase(Locale.ROOT)); } @Bean @@ -267,7 +268,7 @@ public PersonFunction() { @Override public Person apply(Person input) { - return new Person(input.getName().toUpperCase()); + return new Person(input.getName().toUpperCase(Locale.ROOT)); } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java index b91d88496..9ec7ee46c 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/java/org/springframework/cloud/function/adapter/aws/FunctionInvokerTests.java @@ -25,6 +25,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -44,7 +45,9 @@ import com.amazonaws.services.lambda.runtime.events.S3Event; import com.amazonaws.services.lambda.runtime.events.SNSEvent; import com.amazonaws.services.lambda.runtime.events.SQSEvent; +import com.amazonaws.services.lambda.runtime.events.ScheduledEvent; import com.fasterxml.jackson.databind.ObjectMapper; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -63,7 +66,6 @@ import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; /** * @@ -78,6 +80,65 @@ public class FunctionInvokerTests { String jsonPojoCollection = "[{\"name\":\"Ricky\"},{\"name\":\"Julien\"},{\"name\":\"Julien\"}]"; + String someEvent = "{\n" + + " \"payload\": {\n" + + " \"headers\": {\n" + + " \"businessUnit\": \"1\"\n" + + " }\n" + + " },\n" + + " \"headers\": {\n" + + " \"aws-context\": {\n" + + " \"memoryLimit\": 1024,\n" + + " \"awsRequestId\": \"87a211bf-540f-4f9f-a218-d096a0099999\",\n" + + " \"functionName\": \"myfunction\",\n" + + " \"functionVersion\": \"278\",\n" + + " \"invokedFunctionArn\": \"arn:aws:lambda:us-east-1:xxxxxxx:function:xxxxx:snapstart\",\n" + + " \"deadlineTimeInMs\": 1712717704761,\n" + + " \"logger\": {\n" + + " \"logFiltering\": {\n" + + " \"minimumLogLevel\": \"UNDEFINED\"\n" + + " },\n" + + " \"logFormatter\": {},\n" + + " \"logFormat\": \"TEXT\"\n" + + " }\n" + + " },\n" + + " \"businessUnit\": \"1\",\n" + + " \"id\": \"xxxx\",\n" + + " \"aws-event\": true,\n" + + " \"timestamp\": 1712716805129\n" + + " }\n" + + "}"; + + String scheduleEvent = "{\n" + + " \"version\": \"0\",\n" + + " \"id\": \"17793124-05d4-b198-2fde-7ededc63b103\",\n" + + " \"detail-type\": \"Object Created\",\n" + + " \"source\": \"aws.s3\",\n" + + " \"account\": \"111122223333\",\n" + + " \"time\": \"2021-11-12T00:00:00Z\",\n" + + " \"region\": \"ca-central-1\",\n" + + " \"resources\": [\n" + + " \"arn:aws:s3:::amzn-s3-demo-bucket1\"\n" + + " ],\n" + + " \"detail\": {\n" + + " \"version\": \"0\",\n" + + " \"bucket\": {\n" + + " \"name\": \"amzn-s3-demo-bucket1\"\n" + + " },\n" + + " \"object\": {\n" + + " \"key\": \"example-key\",\n" + + " \"size\": 5,\n" + + " \"etag\": \"b1946ac92492d2347c6235b4d2611184\",\n" + + " \"version-id\": \"IYV3p45BT0ac8hjHg1houSdS1a.Mro8e\",\n" + + " \"sequencer\": \"617f08299329d189\"\n" + + " },\n" + + " \"request-id\": \"N4N7GDK58NMKJ12R\",\n" + + " \"requester\": \"123456789012\",\n" + + " \"source-ip-address\": \"1.2.3.4\",\n" + + " \"reason\": \"PutObject\"\n" + + " }\n" + + "} "; + String dynamoDbEvent = "{\n" + " \"Records\": [\n" + " {\n" @@ -706,6 +767,35 @@ public void before() throws Exception { //this.getEnvironment().clear(); } + @Test + public void testScheduledEvent() throws Exception { + System.setProperty("MAIN_CLASS", ScheduledEventConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "event"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.scheduleEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + String result = new String(output.toByteArray(), StandardCharsets.UTF_8); + assertThat(result).contains("IYV3p45BT0ac8hjHg1houSdS1a.Mro8e"); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testConversionWhenPayloadExists() throws Exception { + System.setProperty("MAIN_CLASS", BasicConfiguration.class.getName()); + System.setProperty("spring.cloud.function.definition", "uppercase"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.someEvent.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + + Map result = mapper.readValue(output.toByteArray(), Map.class); + assertThat(result).containsKey("HEADERS"); + + } + @Test public void testAPIGatewayCustomAuthorizerEvent() throws Exception { System.setProperty("MAIN_CLASS", AuthorizerConfiguration.class.getName()); @@ -953,6 +1043,18 @@ public void testS3StringEvent() throws Exception { assertThat(result).contains("s3SchemaVersion"); } + @Test + public void testS3EventAsOutput() throws Exception { + System.setProperty("MAIN_CLASS", S3Configuration.class.getName()); + System.setProperty("spring.cloud.function.definition", "outputS3Event"); + FunctionInvoker invoker = new FunctionInvoker(); + + InputStream targetStream = new ByteArrayInputStream(this.s3Event.getBytes()); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + invoker.handleRequest(targetStream, output, null); + assertThat(output.toByteArray()).isNotNull(); + } + @Test public void testS3Event() throws Exception { System.setProperty("MAIN_CLASS", S3Configuration.class.getName()); @@ -1404,7 +1506,7 @@ public void testWithDefaultRoutingFailure() throws Exception { try { invoker.handleRequest(targetStream, output, null); - fail(); + Assertions.fail(); } catch (Exception e) { // TODO: handle exception @@ -1441,6 +1543,17 @@ public void testPrimitiveMessage() throws Exception { assertThat(result).isEqualTo(testString); } + @EnableAutoConfiguration + @Configuration + public static class BasicConfiguration { + @Bean + public Function, Message> uppercase() { + return v -> { + return MessageBuilder.withPayload(v.getPayload().toUpperCase(Locale.ROOT)).build(); + }; + } + } + @EnableAutoConfiguration @Configuration public static class AuthorizerConfiguration { @@ -1469,7 +1582,7 @@ public Function echoString() { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean @@ -1622,6 +1735,13 @@ public Function, String> inputSNSEventAsMap() { @EnableAutoConfiguration @Configuration public static class S3Configuration { + + @Bean + public Function outputS3Event() { + return v -> { + return v; + }; + } @Bean public Function echoString() { return v -> v; @@ -1728,7 +1848,7 @@ public Consumer consume() { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean @@ -1739,7 +1859,7 @@ public Function, Mono> reactiveWithVoidReturn() { @Bean public Function uppercasePojo() { return v -> { - return v.getName().toUpperCase(); + return v.getName().toUpperCase(Locale.ROOT); }; } @@ -1747,7 +1867,7 @@ public Function uppercasePojo() { public Function uppercasePojoReturnPojo() { return v -> { Person p = new Person(); - p.setName(v.getName().toUpperCase()); + p.setName(v.getName().toUpperCase(Locale.ROOT)); return p; }; } @@ -1756,7 +1876,7 @@ public Function uppercasePojoReturnPojo() { public Function, Flux> uppercasePojoReturnPojoReactive() { return flux -> flux.map(v -> { Person p = new Person(); - p.setName(v.getName().toUpperCase()); + p.setName(v.getName().toUpperCase(Locale.ROOT)); return p; }); } @@ -1893,4 +2013,17 @@ public String toString() { return this.name; } } + + @EnableAutoConfiguration + @Configuration + public static class ScheduledEventConfiguration { + + @Bean + public Function event() { + return event -> { + System.out.println("Event: " + event); + return event; + }; + } + } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/resources/META-INF/mask.keys b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/resources/META-INF/mask.keys new file mode 100644 index 000000000..1c8ec5990 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/test/resources/META-INF/mask.keys @@ -0,0 +1 @@ +eventSourceARN, ApproximateCreationDateTime \ No newline at end of file diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml index a8903600e..9b943497b 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/pom.xml @@ -9,7 +9,7 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT UTF-8 diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/main/java/org/springframework/cloud/function/adapter/azure/web/AzureWebProxyInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/main/java/org/springframework/cloud/function/adapter/azure/web/AzureWebProxyInvoker.java index 82c245c3d..79c66207a 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/main/java/org/springframework/cloud/function/adapter/azure/web/AzureWebProxyInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/main/java/org/springframework/cloud/function/adapter/azure/web/AzureWebProxyInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.util.Map.Entry; import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; import com.microsoft.azure.functions.ExecutionContext; import com.microsoft.azure.functions.HttpMethod; @@ -48,6 +49,7 @@ * * @author Christian Tzolov * @author Oleg Zhurakousky + * @author Omer Celik * */ public class AzureWebProxyInvoker implements FunctionInstanceInjector { @@ -62,6 +64,8 @@ public class AzureWebProxyInvoker implements FunctionInstanceInjector { private ServletContext servletContext; + private static final ReentrantLock globalLock = new ReentrantLock(); + @SuppressWarnings("unchecked") @Override public T getInstance(Class functionClass) throws Exception { @@ -72,13 +76,20 @@ public T getInstance(Class functionClass) throws Exception { /** * Because the getInstance is called by Azure Java Function on every function request we need to cache the Spring * context initialization on the first function call. + * Double-Checked Locking Optimization was used to avoid unnecessary locking overhead. * @throws ServletException error. */ private void initialize() throws ServletException { - synchronized (AzureWebProxyInvoker.class.getName()) { - if (mvc == null) { - Class startClass = FunctionClassUtils.getStartClass(); - this.mvc = ServerlessMVC.INSTANCE(startClass); + if (mvc == null) { + try { + globalLock.lock(); + if (mvc == null) { + Class startClass = FunctionClassUtils.getStartClass(); + this.mvc = ServerlessMVC.INSTANCE(startClass); + } + } + finally { + globalLock.unlock(); } } } @@ -115,8 +126,13 @@ private HttpServletRequest prepareRequest(HttpRequestMessage> r @FunctionName(AZURE_WEB_ADAPTER_NAME) public HttpResponseMessage execute( - @HttpTrigger(name = "req", methods = { HttpMethod.GET, - HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS, route = AZURE_WEB_ADAPTER_ROUTE) HttpRequestMessage> request, + @HttpTrigger(name = "req", methods = { + HttpMethod.GET, + HttpMethod.POST, + HttpMethod.PUT, + HttpMethod.DELETE, + HttpMethod.PATCH + }, authLevel = AuthorizationLevel.ANONYMOUS, route = AZURE_WEB_ADAPTER_ROUTE) HttpRequestMessage> request, ExecutionContext context) { context.getLogger().info("Request body is: " + request.getBody().orElse("[empty]")); @@ -127,7 +143,8 @@ public HttpResponseMessage execute( try { this.mvc.service(httpRequest, httpResponse); - Builder responseBuilder = request.createResponseBuilder(HttpStatus.OK); + HttpStatus status = HttpStatus.valueOf(httpResponse.getStatus()); + Builder responseBuilder = request.createResponseBuilder(status); for (String headerName : httpResponse.getHeaderNames()) { responseBuilder.header(headerName, httpResponse.getHeader(headerName)); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/test/java/org/springframework/cloud/function/adapter/azure/web/PetData.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/test/java/org/springframework/cloud/function/adapter/azure/web/PetData.java index 59989bf8a..7bfeb5241 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/test/java/org/springframework/cloud/function/adapter/azure/web/PetData.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure-web/src/test/java/org/springframework/cloud/function/adapter/azure/web/PetData.java @@ -24,7 +24,10 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; -public class PetData { +public final class PetData { + private PetData() { + + } private static List breeds = new ArrayList<>(); static { breeds.add("Afghan Hound"); 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 a639d76cc..75ab33df1 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 @@ -12,7 +12,7 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT 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 index 999d24c01..d83dd6e78 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 the original author or authors. + * Copyright 2021-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.cloud.function.adapter.azure; import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; import com.microsoft.azure.functions.spi.inject.FunctionInstanceInjector; import org.apache.commons.logging.Log; @@ -37,6 +38,7 @@ * 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 + * @author Omer Celik * @since 3.2.9 */ public class AzureFunctionInstanceInjector implements FunctionInstanceInjector { @@ -45,6 +47,8 @@ public class AzureFunctionInstanceInjector implements FunctionInstanceInjector { private static ConfigurableApplicationContext APPLICATION_CONTEXT; + private static final ReentrantLock globalLock = new ReentrantLock(); + /** * 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 @@ -83,13 +87,20 @@ public T getInstance(Class functionClass) throws Exception { /** * Create a static Application Context instance shared between multiple function invocations. + * Double-Checked Locking Optimization was used to avoid unnecessary locking overhead. */ private static void initialize() { - synchronized (AzureFunctionInstanceInjector.class.getName()) { - if (APPLICATION_CONTEXT == null) { - Class springConfigurationClass = FunctionClassUtils.getStartClass(); - logger.info("Initializing: " + springConfigurationClass); - APPLICATION_CONTEXT = springApplication(springConfigurationClass).run(); + if (APPLICATION_CONTEXT == null) { + try { + globalLock.lock(); + if (APPLICATION_CONTEXT == null) { + Class springConfigurationClass = FunctionClassUtils.getStartClass(); + logger.info("Initializing: " + springConfigurationClass); + APPLICATION_CONTEXT = springApplication(springConfigurationClass).run(); + } + } + finally { + globalLock.unlock(); } } } 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 decdd24c2..1ba8fbe6a 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-2022 the original author or authors. + * Copyright 2021-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; import com.fasterxml.jackson.databind.ObjectMapper; import com.microsoft.azure.functions.ExecutionContext; @@ -66,6 +67,7 @@ * @author Oleg Zhurakousky * @author Chris Bono * @author Christian Tzolov + * @author Omer Celik * * @since 3.2 * @@ -85,6 +87,8 @@ public class FunctionInvoker { private static JsonMapper OBJECT_MAPPER; + private static final ReentrantLock globalLock = new ReentrantLock(); + public FunctionInvoker(Class configurationClass) { try { initialize(configurationClass); @@ -355,30 +359,38 @@ private MessageHeaders getHeaders(HttpRequestMessage event) { return new MessageHeaders(headers); } + /** + * Double-Checked Locking Optimization was used to avoid unnecessary locking overhead. + */ private static void initialize(Class configurationClass) { - synchronized (FunctionInvoker.class.getName()) { - if (FUNCTION_CATALOG == null) { - logger.info("Initializing: " + configurationClass); - SpringApplication builder = springApplication(configurationClass); - APPLICATION_CONTEXT = builder.run(); - - Map mf = APPLICATION_CONTEXT.getBeansOfType(FunctionCatalog.class); - if (CollectionUtils.isEmpty(mf)) { - OBJECT_MAPPER = new JacksonMapper(new ObjectMapper()); - JsonMessageConverter jsonConverter = new JsonMessageConverter(OBJECT_MAPPER); - SmartCompositeMessageConverter messageConverter = new SmartCompositeMessageConverter( + if (FUNCTION_CATALOG == null) { + try { + globalLock.lock(); + if (FUNCTION_CATALOG == null) { + logger.info("Initializing: " + configurationClass); + SpringApplication builder = springApplication(configurationClass); + APPLICATION_CONTEXT = builder.run(); + + Map mf = APPLICATION_CONTEXT.getBeansOfType(FunctionCatalog.class); + if (CollectionUtils.isEmpty(mf)) { + OBJECT_MAPPER = new JacksonMapper(new ObjectMapper()); + JsonMessageConverter jsonConverter = new JsonMessageConverter(OBJECT_MAPPER); + SmartCompositeMessageConverter messageConverter = new SmartCompositeMessageConverter( Collections.singletonList(jsonConverter)); - FUNCTION_CATALOG = new SimpleFunctionRegistry( + FUNCTION_CATALOG = new SimpleFunctionRegistry( APPLICATION_CONTEXT.getBeanFactory().getConversionService(), messageConverter, OBJECT_MAPPER); - } - else { - OBJECT_MAPPER = APPLICATION_CONTEXT.getBean(JsonMapper.class); - FUNCTION_CATALOG = mf.values().iterator().next(); + } + else { + OBJECT_MAPPER = APPLICATION_CONTEXT.getBean(JsonMapper.class); + FUNCTION_CATALOG = mf.values().iterator().next(); + } } } + finally { + globalLock.unlock(); + } } - } private static SpringApplication springApplication(Class configurationClass) { 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 index 2299007cc..ce639cf95 100644 --- 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 @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.function.Function; import java.util.stream.Collectors; @@ -184,7 +185,7 @@ static class TestFunctionsConfig { @Bean public Function imperativeUppercase() { - return (s) -> s.toUpperCase(); + return (s) -> s.toUpperCase(Locale.ROOT); } @Bean diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java index a010af1d0..5ed105fb4 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -195,7 +196,7 @@ public Function, Flux> echoStream() { @Bean public Function, Mono> uppercaseMono() { - return f -> f.map(v -> v.toUpperCase()); + return f -> f.map(v -> v.toUpperCase(Locale.ROOT)); } } @@ -241,7 +242,7 @@ protected static class BareConfig { @Bean("uppercase") public Function, Flux> function() { - return foos -> foos.map(foo -> new Bar(foo.getValue().toUpperCase())); + return foos -> foos.map(foo -> new Bar(foo.getValue().toUpperCase(Locale.ROOT))); } } @@ -256,7 +257,7 @@ public Function, Bar> uppercase() { Foo foo = message.getPayload(); ExecutionContext targetContext = (ExecutionContext) message.getHeaders().get("executionContext"); targetContext.getLogger().info("Invoking 'uppercase' on " + foo.getValue()); - return new Bar(foo.getValue().toUpperCase()); + return new Bar(foo.getValue().toUpperCase(Locale.ROOT)); }; } @@ -269,7 +270,7 @@ protected static class ListConfig { @Bean public Function, List> uppercase() { return foos -> { - List bars = foos.stream().map(foo -> new Bar(foo.getValue().toUpperCase())) + List bars = foos.stream().map(foo -> new Bar(foo.getValue().toUpperCase(Locale.ROOT))) .collect(Collectors.toList()); return bars; }; @@ -283,7 +284,7 @@ protected static class CollectConfig { @Bean public Function, Bar> uppercase() { - return foos -> new Bar(foos.stream().map(foo -> foo.getValue().toUpperCase()) + return foos -> new Bar(foos.stream().map(foo -> foo.getValue().toUpperCase(Locale.ROOT)) .collect(Collectors.joining(","))); } @@ -300,7 +301,7 @@ public Function, Bar> uppercase() { ExecutionContext context = (ExecutionContext) message.getHeaders().get("executionContext"); Foo foo = message.getPayload(); context.getLogger().info("Executing uppercase function"); - return new Bar(foo.getValue().toUpperCase()); + return new Bar(foo.getValue().toUpperCase(Locale.ROOT)); }; } @@ -310,7 +311,7 @@ public Function, Foo> lowercase() { ExecutionContext context = (ExecutionContext) message.getHeaders().get("executionContext"); Bar bar = message.getPayload(); context.getLogger().info("Executing lowercase function"); - return new Foo(bar.getValue().toLowerCase()); + return new Foo(bar.getValue().toLowerCase(Locale.ROOT)); }; } @@ -330,11 +331,11 @@ class Foo { } public String lowercase() { - return this.value.toLowerCase(); + return this.value.toLowerCase(Locale.ROOT); } public String uppercase() { - return this.value.toUpperCase(); + return this.value.toUpperCase(Locale.ROOT); } public String getValue() { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java index 20445fdc4..82a3dfed0 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java @@ -21,6 +21,7 @@ import java.net.URISyntaxException; import java.util.Collections; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -122,7 +123,7 @@ public Function, Message> function() { return (foo -> { Map headers = new HashMap<>(); return new GenericMessage<>( - new Bar(foo.getPayload().getValue().toUpperCase()), headers); + new Bar(foo.getPayload().getValue().toUpperCase(Locale.ROOT)), headers); }); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java index 11dfa8c48..c4147e742 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/AzureFunctionInstanceInjectorTest.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.adapter.azure.injector; +import java.util.Locale; import java.util.Optional; import java.util.function.Function; @@ -106,7 +107,7 @@ public Function, String> uppercaseBean() { Assertions.assertThat(context).isNotNull(); Assertions.assertThat(context.getFunctionName()).isEqualTo("hello"); - return message.getPayload().toUpperCase(); + return message.getPayload().toUpperCase(Locale.ROOT); }; } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/FunctionInstanceInjectorServiceLoadingTest.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/FunctionInstanceInjectorServiceLoadingTest.java index 0af13fcd7..b21eb3900 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/FunctionInstanceInjectorServiceLoadingTest.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/injector/FunctionInstanceInjectorServiceLoadingTest.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.adapter.azure.injector; import java.util.Iterator; +import java.util.Locale; import java.util.Optional; import java.util.ServiceLoader; import java.util.function.Function; @@ -108,7 +109,7 @@ public Function, String> uppercase() { .get(AzureFunctionUtil.EXECUTION_CONTEXT); Assertions.assertThat(context).isNotNull(); Assertions.assertThat(context.getFunctionName()).isEqualTo("hello"); - return message.getPayload().toUpperCase(); + return message.getPayload().toUpperCase(Locale.ROOT); }; } } 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 2b6729471..6e73b5aa3 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 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java index 062787d84..a10aa2ac4 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/FunctionInvoker.java @@ -17,8 +17,11 @@ package org.springframework.cloud.function.adapter.gcp; import java.io.BufferedReader; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.Map.Entry; import java.util.function.Consumer; import java.util.function.Function; @@ -32,6 +35,8 @@ import com.google.cloud.functions.RawBackgroundFunction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; @@ -40,6 +45,7 @@ import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; import org.springframework.cloud.function.context.config.RoutingFunction; +import org.springframework.cloud.function.json.JsonMapper; import org.springframework.cloud.function.utils.FunctionClassUtils; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.messaging.Message; @@ -76,6 +82,8 @@ public class FunctionInvoker implements HttpFunction, RawBackgroundFunction { private ConfigurableApplicationContext context; + private JsonMapper jsonMapper; + public FunctionInvoker() { this(FunctionClassUtils.getStartClass()); } @@ -90,18 +98,19 @@ private void init(Class configurationClass) { System.setProperty(ContextFunctionCatalogAutoConfiguration.JSON_MAPPER_PROPERTY, "gson"); } Thread.currentThread() // TODO: remove after upgrading to 1.0.0-alpha-2-rc5 - .setContextClassLoader(FunctionInvoker.class.getClassLoader()); + .setContextClassLoader(FunctionInvoker.class.getClassLoader()); log.info("Initializing: " + configurationClass); SpringApplication springApplication = springApplication(configurationClass); this.context = springApplication.run(); this.catalog = this.context.getBean(FunctionCatalog.class); + this.jsonMapper = this.context.getBean(JsonMapper.class); initFunctionConsumerOrSupplierFromCatalog(); } private Function, Message> lookupFunction() { Function, Message> function = this.catalog.lookup(functionName, - MimeTypeUtils.APPLICATION_JSON.toString()); + MimeTypeUtils.APPLICATION_JSON.toString()); Assert.notNull(function, "'function' with name '" + functionName + "' must not be null"); return function; } @@ -114,35 +123,17 @@ private Function, Message> lookupFunction() { public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws Exception { Function, Message> function = lookupFunction(); - Message message = this.functionWrapped.getInputType() == Void.class || this.functionWrapped.getInputType() == null ? null - : MessageBuilder.withPayload(httpRequest.getReader()).copyHeaders(httpRequest.getHeaders()).build(); + Message message = this.functionWrapped.getInputType() == Void.class + || this.functionWrapped.getInputType() == null ? null + : MessageBuilder.withPayload(httpRequest.getReader()).copyHeaders(httpRequest.getHeaders()) + .build(); - Message result = function.apply(message); + Object resultObject = function.apply(message); - if (result != null) { - MessageHeaders headers = result.getHeaders(); - httpResponse.setContentType(result.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString()); - httpResponse.getWriter().write(new String(result.getPayload(), StandardCharsets.UTF_8)); - for (Entry header : headers.entrySet()) { - Object values = header.getValue(); - if (values instanceof Collection) { - String headerValue = ((Collection) values).stream().map(item -> item.toString()).collect(Collectors.joining(",")); - httpResponse.appendHeader(header.getKey(), headerValue); - } - else { - httpResponse.appendHeader(header.getKey(), header.getValue().toString()); - } - } - httpRequest.getContentType().ifPresent(contentType -> httpResponse.setContentType(contentType)); + if (resultObject != null) { + Message result = resultObject instanceof Publisher ? getResultFromPublisher(resultObject) : (Message) resultObject; - if (headers.containsKey(HTTP_STATUS_CODE)) { - if (headers.get(HTTP_STATUS_CODE) instanceof Integer) { - httpResponse.setStatusCode((int) headers.get(HTTP_STATUS_CODE)); - } - else { - log.warn("The statusCode should be an Integer value"); - } - } + buildHttpResponse(httpRequest, httpResponse, result); } } @@ -154,19 +145,115 @@ public void service(HttpRequest httpRequest, HttpResponse httpResponse) throws E * @param context event context. * @since 3.0.5 */ + @SuppressWarnings("unchecked") @Override public void accept(String json, Context context) { Function, Message> function = lookupFunction(); Message message = this.functionWrapped.getInputType() == Void.class ? null - : MessageBuilder.withPayload(json).setHeader("gcf_context", context).build(); + : MessageBuilder.withPayload(json).setHeader("gcf_context", context).build(); - Message result = function.apply(message); + Object resultObject = function.apply(message); + + Message result = null; + if (resultObject instanceof Publisher) { + result = getResultFromPublisher(resultObject); + } + else { + result = (Message) resultObject; + } if (result != null) { log.info("Dropping background function result: " + new String(result.getPayload())); } } + /* + * This method build the http response from service. + */ + private void buildHttpResponse(HttpRequest httpRequest, HttpResponse httpResponse, Message result) + throws IOException { + MessageHeaders headers = result.getHeaders(); + if (result.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)) { + httpResponse.setContentType(result.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString()); + } + else if (result.getHeaders().containsKey("Content-Type")) { + httpResponse.setContentType(result.getHeaders().get("Content-Type").toString()); + } + else { + httpRequest.getContentType().ifPresent(contentType -> httpResponse.setContentType(contentType)); + } + String content = result.getPayload() instanceof String strPayload ? strPayload + : new String((byte[]) result.getPayload(), StandardCharsets.UTF_8); + httpResponse.getWriter().write(content); + for (Entry header : headers.entrySet()) { + Object values = header.getValue(); + if (values instanceof Collection) { + String headerValue = ((Collection) values).stream().map(item -> item.toString()) + .collect(Collectors.joining(",")); + httpResponse.appendHeader(header.getKey(), headerValue); + } + else { + httpResponse.appendHeader(header.getKey(), header.getValue().toString()); + } + } + + if (headers.containsKey(HTTP_STATUS_CODE)) { + if (headers.get(HTTP_STATUS_CODE) instanceof Integer) { + httpResponse.setStatusCode((int) headers.get(HTTP_STATUS_CODE)); + } + else { + log.warn("The statusCode should be an Integer value"); + } + } + } + + /* + * This methd get the result from reactor's publisher. + * + * For reference: https://github.com/spring-cloud/spring-cloud-function/blob/main/spring-cloud-function-adapters/spring-cloud-function-adapter-aws/src/main/java/org/springframework/cloud/function/adapter/aws/AWSLambdaUtils.java + */ + private Message getResultFromPublisher(Object resultObject) { + List results = new ArrayList<>(); + Message lastMessage = null; + for (Object item : Flux.from((Publisher) resultObject).toIterable()) { + log.info("Response value: " + item); + if (item instanceof Message messageItem) { + results.add(convertFromJsonIfNecessary(messageItem.getPayload())); + lastMessage = messageItem; + } + else { + results.add(convertFromJsonIfNecessary(item)); + } + } + + byte[] resultsPayload; + if (results.size() == 1) { + resultsPayload = jsonMapper.toJson(results.get(0)); + } + else if (results.size() > 1) { + resultsPayload = jsonMapper.toJson(results); + } + else { + resultsPayload = null; + } + + Assert.notNull(resultsPayload, "Couldn't resolve payload result"); + + MessageBuilder messageBuilder = MessageBuilder.withPayload(resultsPayload); + if (lastMessage != null) { + messageBuilder.copyHeaders(lastMessage.getHeaders()); + } + return messageBuilder.build(); + } + + private Object convertFromJsonIfNecessary(Object value) { + if (JsonMapper.isJsonString(value)) { + return jsonMapper.fromJson(value, Object.class); + } + + return value; + } + private void initFunctionConsumerOrSupplierFromCatalog() { String name = resolveName(Function.class); this.functionWrapped = this.catalog.lookup(Function.class, name); diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/layout/GcfJarLayout.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/layout/GcfJarLayout.java index 874e0c66a..5f80baa03 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/layout/GcfJarLayout.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/main/java/org/springframework/cloud/function/adapter/gcp/layout/GcfJarLayout.java @@ -21,6 +21,7 @@ import org.springframework.boot.loader.tools.CustomLoaderLayout; import org.springframework.boot.loader.tools.Layouts; import org.springframework.boot.loader.tools.LoaderClassesWriter; +import org.springframework.boot.loader.tools.LoaderImplementation; import org.springframework.cloud.function.adapter.gcp.GcfJarLauncher; /** @@ -46,7 +47,7 @@ public boolean isExecutable() { @Override public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException { - writer.writeLoaderClasses(); + writer.writeLoaderClasses(LoaderImplementation.CLASSIC); String jarName = LAUNCHER_NAME.replaceAll("\\.", "/") + ".class"; writer.writeEntry( diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerBackgroundTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerBackgroundTests.java index 29951e594..0c4bc4698 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerBackgroundTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerBackgroundTests.java @@ -24,6 +24,8 @@ import com.google.gson.Gson; import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; import org.springframework.cloud.function.json.JsonMapper; @@ -58,6 +60,18 @@ public void testJsonInputFunction_Background(CaptureSystemOutput.OutputCapture o "Thank you for sending the message: hello", null, null); } + @Test + public void testJsonInputFunction_BackgroundMono(CaptureSystemOutput.OutputCapture outputCapture) { + testBackgroundFunction(outputCapture, JsonInputFunctionMono.class, new IncomingRequest("hello"), + "Thank you for sending the message: hello", null, null); + } + + @Test + public void testJsonInputFunction_BackgroundFlux(CaptureSystemOutput.OutputCapture outputCapture) { + testBackgroundFunction(outputCapture, JsonInputFunctionFlux.class, new IncomingRequest("hello"), + "Thank you for sending the message: hello", null, null); + } + @Test public void testJsonInputOutputFunction_Background(CaptureSystemOutput.OutputCapture outputCapture) { testBackgroundFunction(outputCapture, JsonInputOutputFunction.class, new IncomingRequest("hello"), @@ -149,6 +163,28 @@ public Function function() { } + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class JsonInputFunctionMono { + + @Bean + public Function, Mono> function() { + return (in) -> in.map(o -> "Thank you for sending the message: " + o.message); + } + + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class JsonInputFunctionFlux { + + @Bean + public Function, Flux> function() { + return (in) -> in.map(o -> "Thank you for sending the message: " + o.message); + } + + } + @Configuration @Import({ ContextFunctionCatalogAutoConfiguration.class }) protected static class JsonInputOutputFunction { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java index c178414ae..51f1324e7 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/FunctionInvokerHttpTests.java @@ -34,6 +34,8 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; @@ -42,6 +44,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; import static java.util.Arrays.asList; @@ -89,6 +92,36 @@ public void testHelloWorldSupplier() throws Exception { } + @Test + public void testJsonInputFunctionMono() throws Exception { + + FunctionInvoker handler = new FunctionInvoker(JsonInputFunctionMono.class); + + String expectedOutput = "Thank you for sending the message: hello"; + IncomingRequest input = new IncomingRequest("hello"); + + when(request.getReader()).thenReturn(new BufferedReader(new StringReader(gson.toJson(input)))); + handler.service(request, response); + bufferedWriter.close(); + + + assertThat(writer.toString()).isEqualTo(gson.toJson(expectedOutput)); + } + + @Test + public void testJsonInputFunctionFlux() throws Exception { + + FunctionInvoker handler = new FunctionInvoker(JsonInputFunctionFlux.class); + + String expectedOutput = "hello!!!"; + + when(request.getReader()).thenReturn(new BufferedReader(new StringReader("hello"))); + handler.service(request, response); + bufferedWriter.close(); + + + assertThat(writer.toString()).isEqualTo(gson.toJson(expectedOutput)); + } @Test public void testJsonInputFunction() throws Exception { @@ -165,7 +198,7 @@ public void testStatusCodeSet() throws Exception { bufferedWriter.close(); verify(response).setStatusCode(404); - + verify(response).setContentType("text/plain"); } @Test @@ -200,7 +233,7 @@ public Function> function() { String payload = "hello"; - Message msg = MessageBuilder.withPayload(payload).setHeader("statusCode", 404) + Message msg = MessageBuilder.withPayload(payload).setHeader("statusCode", 404).setHeader(MessageHeaders.CONTENT_TYPE, "text/plain") .build(); return x -> msg; @@ -237,6 +270,28 @@ public Function function() { } + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class JsonInputFunctionMono { + + @Bean + public Function, Mono> function() { + return (in) -> in.map(o -> "Thank you for sending the message: " + o.message); + } + + } + + @Configuration + @Import({ ContextFunctionCatalogAutoConfiguration.class }) + protected static class JsonInputFunctionFlux { + + @Bean + public Function, Flux> function() { + return (in) -> in.map(word -> word + "!!!"); + } + + } + @Configuration @Import({ ContextFunctionCatalogAutoConfiguration.class }) protected static class JsonInputOutputFunction { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/integration/FunctionInvokerIntegrationTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/integration/FunctionInvokerIntegrationTests.java index 054de3b1f..fce82e875 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/integration/FunctionInvokerIntegrationTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-gcp/src/test/java/org/springframework/cloud/function/adapter/gcp/integration/FunctionInvokerIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.adapter.gcp.integration; import java.io.IOException; +import java.util.Locale; import java.util.function.Function; import java.util.function.Supplier; @@ -98,7 +99,7 @@ static class CloudFunctionMainSingular { @Bean Function uppercase() { - return input -> input.toUpperCase(); + return input -> input.toUpperCase(Locale.ROOT); } } @@ -109,7 +110,7 @@ static class CloudFunctionMain { @Bean Function uppercase() { - return input -> input.toUpperCase(); + return input -> input.toUpperCase(Locale.ROOT); } @Bean diff --git a/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/org.springframework.cloud.function.aws-lambda.packaging.gradle.plugin/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/org.springframework.cloud.function.aws-lambda.packaging.gradle.plugin/pom.xml index 6a0b9d396..e0feba3be 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/org.springframework.cloud.function.aws-lambda.packaging.gradle.plugin/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/org.springframework.cloud.function.aws-lambda.packaging.gradle.plugin/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud.function.aws-lambda.packaging spring-cloud-function-aws-gradle-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT ${basedir}/../.. diff --git a/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/spring-cloud-function-aws-packaging-gradle-plugin/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/spring-cloud-function-aws-packaging-gradle-plugin/pom.xml index e8a73485b..342f0c996 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/spring-cloud-function-aws-packaging-gradle-plugin/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-aws-gradle-parent/spring-cloud-function-aws-packaging-gradle-plugin/pom.xml @@ -13,7 +13,7 @@ org.springframework.cloud.function.aws-lambda.packaging spring-cloud-function-aws-gradle-parent - 4.1.2-SNAPSHOT + 4.3.0-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 a52ecf59a..986ff9c38 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,7 +5,7 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT spring-cloud-function-grpc-cloudevent-ext spring-cloud-function-grpc-cloudevent-ext @@ -45,6 +45,16 @@ + org.apache.maven.plugins + maven-checkstyle-plugin + + + checkstyle-validation + none + + + + org.xolstice.maven.plugins protobuf-maven-plugin 0.6.1 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 286656c30..ea308f3bc 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-grpc/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT 1.55.1 diff --git a/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcFunctionAutoConfiguration.java b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcFunctionAutoConfiguration.java new file mode 100644 index 000000000..04859acfd --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcFunctionAutoConfiguration.java @@ -0,0 +1,72 @@ +/* + * Copyright 2021-2021 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.grpc; + +import java.util.function.Function; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.function.context.FunctionProperties; +import org.springframework.cloud.function.context.MessageRoutingCallback; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.messaging.Message; + +/** + * + * @author Oleg Zhurakousky + * @since 3.2 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(FunctionGrpcProperties.class) +public class GrpcFunctionAutoConfiguration { + + public static String GRPC_INVOKER_FUNCTION = "grpcInvokerFunction"; + + public static String GRPC = "grpc"; + + public static String GRPC_HOST = "grpcHost"; + + public static String GRPC_PORT = "grpcPort"; + + @Bean + public Function, Message> grpcInvokerFunction() { + return message -> { + if (message.getHeaders().containsKey(GRPC_HOST)) { + String host = (String) message.getHeaders().get(GRPC_HOST); + int port = message.getHeaders().get(GRPC_PORT) instanceof String stringPort + ? Integer.parseInt(stringPort) + : (int)message.getHeaders().get(GRPC_PORT); + + return GrpcUtils.requestReply(host, port, message); + } + return GrpcUtils.requestReply(message); + }; + } + + @Bean + public MessageRoutingCallback routingCallback() { + return new MessageRoutingCallback() { + public String routingResult(Message message) { + if (message.getHeaders().containsKey(FunctionProperties.PROXY) + && message.getHeaders().get(FunctionProperties.PROXY).equals(GRPC)) { + return GRPC_INVOKER_FUNCTION; + } + return null; + } + }; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServerMessageHandler.java b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServerMessageHandler.java index f04405846..74207d4d2 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServerMessageHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/java/org/springframework/cloud/function/grpc/GrpcServerMessageHandler.java @@ -32,36 +32,11 @@ package org.springframework.cloud.function.grpc; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -// -import io.grpc.Status; -import io.grpc.stub.ServerCallStreamObserver; -import io.grpc.stub.StreamObserver; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Sinks; -import reactor.core.publisher.Sinks.Many; -// -import org.springframework.cloud.function.context.FunctionCatalog; -import org.springframework.cloud.function.context.FunctionProperties; -import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.grpc.MessagingServiceGrpc.MessagingServiceImplBase; -import org.springframework.context.SmartLifecycle; -import org.springframework.messaging.Message; -import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import com.google.protobuf.GeneratedMessageV3; -// -//import com.google.protobuf.GeneratedMessage; +import io.grpc.stub.StreamObserver; /** * @@ -72,12 +47,8 @@ @SuppressWarnings("rawtypes") public class GrpcServerMessageHandler extends MessagingServiceImplBase { - private Log logger = LogFactory.getLog(GrpcServerMessageHandler.class); - private final MessageHandlingHelper helper; - private boolean running; - public GrpcServerMessageHandler(MessageHandlingHelper helper) { this.helper = helper; } diff --git a/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 1aee89ea4..258fc4773 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ org.springframework.cloud.function.grpc.GrpcAutoConfiguration +org.springframework.cloud.function.grpc.GrpcFunctionAutoConfiguration diff --git a/spring-cloud-function-adapters/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java index ef0d66c7b..4d429c0ca 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-grpc/src/test/java/org/springframework/cloud/function/grpc/GrpcInteractionTests.java @@ -19,6 +19,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Random; import java.util.function.Function; @@ -49,7 +50,7 @@ * @author Oleg Zhurakousky * @author Chris Bono */ -@Disabled +//@Disabled public class GrpcInteractionTests { @BeforeEach @@ -210,6 +211,7 @@ public void testBidirectionalStreamWithReactiveFunction() throws Exception { } @Test + @Disabled public void testClientStreaming() throws Exception { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( SampleConfiguration.class).web(WebApplicationType.NONE).run( @@ -323,17 +325,17 @@ public static class SampleConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean public Function> uppercaseMonoReturn() { - return v -> Mono.just(v.toUpperCase()); + return v -> Mono.just(v.toUpperCase(Locale.ROOT)); } @Bean public Function> uppercaseFluxReturn() { - return v -> Flux.just(v.toUpperCase(), v.toUpperCase() + "-1", v.toUpperCase() + "-2"); + return v -> Flux.just(v.toUpperCase(Locale.ROOT), v.toUpperCase(Locale.ROOT) + "-1", v.toUpperCase(Locale.ROOT) + "-2"); } @Bean @@ -343,7 +345,7 @@ public Function reverse() { @Bean public Function, Flux> uppercaseReactive() { - return flux -> flux.map(v -> v.toUpperCase()); + return flux -> flux.map(v -> v.toUpperCase(Locale.ROOT)); } @Bean @@ -360,7 +362,7 @@ public Function, String> streamInStringOut() { @Bean public Function> stringInStreamOut() { - return value -> Flux.just(value, value.toUpperCase()); + return value -> Flux.just(value, value.toUpperCase(Locale.ROOT)); } } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml index 82b4bf1f9..435ec0fca 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/pom.xml @@ -10,7 +10,7 @@ org.springframework.cloud spring-cloud-function-adapter-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT UTF-8 @@ -21,7 +21,7 @@ org.springframework spring-webmvc - + org.springframework.boot @@ -34,27 +34,31 @@ test - + - jakarta.servlet - jakarta.servlet-api - provided + jakarta.servlet + jakarta.servlet-api + provided org.springframework.boot spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-freemarker + test + org.springframework.boot spring-boot-starter-web - - org.springframework.boot - spring-boot-starter-tomcat - - - test + + org.springframework.boot + spring-boot-starter-tomcat + + diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/sample/pet-store/pom.xml b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/sample/pet-store/pom.xml index 84a6d5ea6..750507b62 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/sample/pet-store/pom.xml +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/sample/pet-store/pom.xml @@ -90,7 +90,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 package @@ -101,7 +101,7 @@ false + implementation="org.apache.logging.log4j.maven.plugins.shade.transformer.Log4j2PluginCacheFileTransformer"> @@ -109,9 +109,9 @@ - com.github.edwgiz - maven-shade-plugin.log4j2-cachefile-transformer - 2.8.1 + org.apache.logging.log4j + log4j-transform-maven-shade-plugin-extensions + 0.2.0 diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java index 44d6ef454..664a2e842 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/FunctionClassUtils.java @@ -26,8 +26,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -//import org.springframework.boot.SpringBootConfiguration; -//import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.core.KotlinDetector; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java index 9d2cdf7ae..40b2a456f 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAsyncContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.locks.ReentrantLock; import jakarta.servlet.AsyncContext; import jakarta.servlet.AsyncEvent; @@ -39,6 +40,7 @@ * Implementation of Async context for {@link ServerlessMVC}. * * @author Oleg Zhurakousky + * @author Omer Celik */ public class ServerlessAsyncContext implements AsyncContext { private final HttpServletRequest request; @@ -55,6 +57,8 @@ public class ServerlessAsyncContext implements AsyncContext { private final List dispatchHandlers = new ArrayList<>(); + private final ReentrantLock globalLock = new ReentrantLock(); + public ServerlessAsyncContext(ServletRequest request, @Nullable ServletResponse response) { this.request = (HttpServletRequest) request; @@ -64,7 +68,8 @@ public ServerlessAsyncContext(ServletRequest request, @Nullable ServletResponse public void addDispatchHandler(Runnable handler) { Assert.notNull(handler, "Dispatch handler must not be null"); - synchronized (this) { + try { + this.globalLock.lock(); if (this.dispatchedPath == null) { this.dispatchHandlers.add(handler); } @@ -72,6 +77,9 @@ public void addDispatchHandler(Runnable handler) { handler.run(); } } + finally { + this.globalLock.unlock(); + } } @Override @@ -102,10 +110,14 @@ public void dispatch(String path) { @Override public void dispatch(@Nullable ServletContext context, String path) { - synchronized (this) { + try { + this.globalLock.lock(); this.dispatchedPath = path; this.dispatchHandlers.forEach(Runnable::run); } + finally { + this.globalLock.unlock(); + } } @Nullable diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java index 65e78a756..dcef7bb27 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessAutoConfiguration.java @@ -22,8 +22,6 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean; -import org.springframework.boot.web.context.ConfigurableWebServerApplicationContext; import org.springframework.boot.web.server.WebServer; import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.servlet.ServletContextInitializer; @@ -54,7 +52,7 @@ public ServletWebServerFactory servletWebServerFactory() { public static class ServerlessServletWebServerFactory implements ServletWebServerFactory, ApplicationContextAware, InitializingBean { - private ConfigurableWebServerApplicationContext applicationContext; + private ApplicationContext applicationContext; @Override public WebServer getWebServer(ServletContextInitializer... initializers) { @@ -67,16 +65,6 @@ public void stop() throws WebServerException { @Override public void start() throws WebServerException { - if (applicationContext instanceof ServletWebServerApplicationContext servletApplicationContet) { - DispatcherServlet dispatcher = applicationContext.getBean(DispatcherServlet.class); - try { - dispatcher.init(new ProxyServletConfig(servletApplicationContet.getServletContext())); - logger.info("Initalized DispatcherServlet"); - } - catch (Exception e) { - throw new IllegalStateException("Faild to create Spring MVC DispatcherServlet proxy", e); - } - } } @Override @@ -88,20 +76,30 @@ public int getPort() { @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - this.applicationContext = (ConfigurableWebServerApplicationContext) applicationContext; + this.applicationContext = applicationContext; } @Override public void afterPropertiesSet() throws Exception { - if (applicationContext instanceof ServletWebServerApplicationContext servletApplicationContet) { + if (applicationContext instanceof ServletWebServerApplicationContext servletApplicationContext) { logger.info("Configuring Serverless Web Container"); ServerlessServletContext servletContext = new ServerlessServletContext(); - servletApplicationContet.setServletContext(servletContext); - for (ServletContextInitializer beans : new ServletContextInitializerBeans(this.applicationContext)) { - if (!(beans instanceof DispatcherServletRegistrationBean)) { - beans.onStartup(servletContext); - } + servletApplicationContext.setServletContext(servletContext); + DispatcherServlet dispatcher = applicationContext.getBean(DispatcherServlet.class); + try { + logger.info("Initializing DispatcherServlet"); + dispatcher.init(new ProxyServletConfig(servletApplicationContext.getServletContext())); + logger.info("Initialized DispatcherServlet"); } + catch (Exception e) { + throw new IllegalStateException("Failed to create Spring MVC DispatcherServlet proxy", e); + } + for (ServletContextInitializer initializer : new ServletContextInitializerBeans(this.applicationContext)) { + initializer.onStartup(servletContext); + } + } + else { + logger.debug("Skipping Serverless configuration for " + this.applicationContext); } } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java index 77b135555..cdd9bd12e 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2023 the original author or authors. + * Copyright 2023-2025 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. @@ -77,6 +77,7 @@ public class ServerlessHttpServletRequest implements HttpServletRequest { private static final BufferedReader EMPTY_BUFFERED_READER = new BufferedReader(new StringReader("")); + private static final InputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(new byte[0]); /** * Date formats as specified in the HTTP RFC. * @@ -283,7 +284,15 @@ public String getContentType() { @Override public ServletInputStream getInputStream() { - InputStream stream = new ByteArrayInputStream(this.content); + + InputStream stream; + if (this.content == null) { + stream = EMPTY_INPUT_STREAM; + } + else { + stream = new ByteArrayInputStream(this.content); + } + return new ServletInputStream() { boolean finished = false; @@ -718,7 +727,7 @@ public Enumeration getHeaderNames() { } public void setHeader(String name, @Nullable String value) { - this.headers.set(name, value); + this.headers.add(name, value); } public void addHeader(String name, @Nullable String value) { diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java index 853617a63..0908621b6 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessMVC.java @@ -29,6 +29,7 @@ import jakarta.servlet.AsyncContext; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; @@ -77,11 +78,9 @@ public final class ServerlessMVC { private volatile ServletWebServerApplicationContext applicationContext; - private ServletContext servletContext; - private final CountDownLatch contextStartupLatch = new CountDownLatch(1); - private final long initializatioinTimeout; + private final long initializationTimeout; public static ServerlessMVC INSTANCE(Class... componentClasses) { ServerlessMVC mvc = new ServerlessMVC(); @@ -102,7 +101,7 @@ private ServerlessMVC() { if (!StringUtils.hasText(timeoutValue)) { timeoutValue = System.getProperty(INIT_TIMEOUT); } - this.initializatioinTimeout = StringUtils.hasText(timeoutValue) ? Long.valueOf(timeoutValue) : 20000; + this.initializationTimeout = StringUtils.hasText(timeoutValue) ? Long.valueOf(timeoutValue) : 20000; } private void initializeContextAsync(Class... componentClasses) { @@ -136,7 +135,7 @@ public ConfigurableWebApplicationContext getApplicationContext() { public ServletContext getServletContext() { this.waitForContext(); - return this.servletContext; + return this.dispatcher.getServletContext(); } public void stop() { @@ -156,9 +155,7 @@ public void stop() { * @see org.springframework.test.web.servlet.result.MockMvcResultMatchers */ public void service(HttpServletRequest request, HttpServletResponse response) throws Exception { - //this.waitForContext(); - //contextStartupLatch.await(this.initializatioinTimeout, TimeUnit.MILLISECONDS); - Assert.state(this.waitForContext(), "Failed to initialize Application within the specified time of " + this.initializatioinTimeout + " milliseconds. " + Assert.state(this.waitForContext(), "Failed to initialize Application within the specified time of " + this.initializationTimeout + " milliseconds. " + "If you need to increase it, please set " + INIT_TIMEOUT + " environment variable"); this.service(request, response, (CountDownLatch) null); } @@ -190,7 +187,7 @@ public void service(HttpServletRequest request, HttpServletResponse response, Co public boolean waitForContext() { try { - return contextStartupLatch.await(initializatioinTimeout, TimeUnit.MILLISECONDS); + return contextStartupLatch.await(initializationTimeout, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -216,7 +213,6 @@ private static class ProxyFilterChain implements FilterChain { * Create a {@code FilterChain} with Filter's and a Servlet. * * @param servlet the {@link Servlet} to invoke in this {@link FilterChain} - * @param filters the {@link Filter}'s to invoke in this {@link FilterChain} * @since 4.0.x */ ProxyFilterChain(DispatcherServlet servlet) { @@ -269,6 +265,10 @@ public void doFilter(ServletRequest request, ServletResponse response) throws IO this.request = request; this.response = response; + + if (!response.isCommitted() && request.getDispatcherType() != DispatcherType.ASYNC) { + response.flushBuffer(); + } } /** diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java index 921c5941f..024064a5f 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessServletContext.java @@ -16,9 +16,13 @@ package org.springframework.cloud.function.serverless.web; +import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; @@ -104,7 +108,14 @@ public int getEffectiveMinorVersion() { @Override public String getMimeType(String file) { - throw new UnsupportedOperationException("This ServletContext does not represent a running web container"); + String mimeType = null; + try { + mimeType = Files.probeContentType(Paths.get(file)); + } + catch (IOException | InvalidPathException e) { + log("unable to probe for content type " + file, e); + } + return mimeType; } @Override @@ -114,12 +125,12 @@ public Set getResourcePaths(String path) { @Override public URL getResource(String path) throws MalformedURLException { - throw new UnsupportedOperationException("This ServletContext does not represent a running web container"); + return ServerlessServletContext.class.getResource(path); } @Override public InputStream getResourceAsStream(String path) { - return null; + return ServerlessServletContext.class.getResourceAsStream(path); } @Override diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java index c4ca109a0..a1ed042cb 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ServerlessWebApplication.java @@ -20,6 +20,7 @@ import java.io.PrintStream; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.function.Consumer; @@ -157,7 +158,7 @@ private Banner printBanner(ConfigurableEnvironment environment) { ResourceLoader resourceLoader = (this.getResourceLoader() != null) ? this.getResourceLoader() : new DefaultResourceLoader(null); Banner.Mode bannerMode = environment.containsProperty("spring.main.banner-mode") - ? Banner.Mode.valueOf(environment.getProperty("spring.main.banner-mode").trim().toUpperCase()) + ? Banner.Mode.valueOf(environment.getProperty("spring.main.banner-mode").trim().toUpperCase(Locale.ROOT)) : Banner.Mode.CONSOLE; if (bannerMode == Banner.Mode.OFF) { diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/AsyncStartTests.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/AsyncStartTests.java index 95743a839..9547b8cff 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/AsyncStartTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/AsyncStartTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.serverless.web; import jakarta.servlet.http.HttpServletRequest; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -25,7 +26,8 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; + + /** * @author Oleg Zhurakousky @@ -55,7 +57,7 @@ public void testAsyncWithEnvSet() throws Exception { ServerlessHttpServletResponse response = new ServerlessHttpServletResponse(); try { mvc.service(request, response); - fail(); + Assertions.fail(); } catch (Exception e) { assertThat(e).isInstanceOf(IllegalStateException.class); diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java index c0351cac1..71be6ee79 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java @@ -57,6 +57,26 @@ public void after() { this.mvc.stop(); } + @Test + public void validateCaseInsensitiveHeaders() throws Exception { + ServerlessHttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/index"); + request.setHeader("User-Agent", "iOS"); + request.setHeader("uSer-Agent", "FOO"); + request.setContentType("application/json"); + request.setHeader("CoNteNt-tYpe", "text/plain"); + + assertThat(request.getHeader("content-TYPE")).isEqualTo("application/json"); + assertThat(request.getHeader("user-agenT")).isEqualTo("iOS"); + } + + @Test + public void validateFreemarker() throws Exception { + HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/index"); + ServerlessHttpServletResponse response = new ServerlessHttpServletResponse(); + mvc.service(request, response); + assertThat(response.getContentAsString()).contains("

hello from freemarker

"); + } + @Test public void validateAccessDeniedWithCustomHandler() throws Exception { HttpServletRequest request = new ServerlessHttpServletRequest(null, "GET", "/foo/deny"); @@ -141,6 +161,21 @@ public void validatePostWithBody() throws Exception { assertThat(pet.getName()).isNotEmpty(); } + @Test + public void validatePostWithoutBody() throws Exception { + ServerlessHttpServletRequest request = new ServerlessHttpServletRequest(null, "POST", "/pets/"); + request.setContentType("application/json"); + ServerlessHttpServletResponse response = new ServerlessHttpServletResponse(); + try { + mvc.service(request, response); + } + catch (jakarta.servlet.ServletException e) { + assertThat(e.getCause()).isNotInstanceOf(NullPointerException.class); + } + + assertThat(response.getStatus()).isEqualTo(400); // application fail because the pet is empty ;) + } + @Test public void validatePostAsyncWithBody() throws Exception { // System.setProperty("spring.main.banner-mode", "off"); diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/FreemarkerController.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/FreemarkerController.java new file mode 100644 index 000000000..bcbb76bc7 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/FreemarkerController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023-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. + * 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.test.app; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class FreemarkerController { + + public FreemarkerController() { + } + + @GetMapping("/index") + public String something2() { + return "index"; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetData.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetData.java index 90b2a736f..ac00af9ef 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetData.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetData.java @@ -24,8 +24,13 @@ import java.util.List; import java.util.concurrent.ThreadLocalRandom; -public class PetData { +public final class PetData { private static List breeds = new ArrayList<>(); + + private PetData() { + + } + static { breeds.add("Afghan Hound"); breeds.add("Beagle"); diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetStoreSpringAppConfig.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetStoreSpringAppConfig.java index 8a5bdfbdf..2a02c3267 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetStoreSpringAppConfig.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetStoreSpringAppConfig.java @@ -53,7 +53,7 @@ @Configuration -@Import({ PetsController.class }) +@Import({ PetsController.class, FreemarkerController.class }) @EnableWebSecurity @EnableAutoConfiguration public class PetStoreSpringAppConfig { @@ -129,6 +129,9 @@ public AnotherFilter anotherFilter() { } public static class SimpleFilter extends OncePerRequestFilter { + /** + * + */ public boolean invoked; @Override @@ -146,6 +149,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } public static class AnotherFilter extends OncePerRequestFilter { + + /** + * + */ public boolean invoked; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/resources/templates/index.ftlh b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/resources/templates/index.ftlh new file mode 100644 index 000000000..3631fde40 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/resources/templates/index.ftlh @@ -0,0 +1,5 @@ +

hello from freemarker

+ +<#list 1..10 as x> + ${x} + \ No newline at end of file diff --git a/spring-cloud-function-context/pom.xml b/spring-cloud-function-context/pom.xml index 86c6f9407..30feeb20f 100644 --- a/spring-cloud-function-context/pom.xml +++ b/spring-cloud-function-context/pom.xml @@ -12,18 +12,13 @@ org.springframework.cloud spring-cloud-function-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT 1.10.2 - - net.jodah - typetools - 0.6.2 - org.springframework.boot spring-boot-autoconfigure @@ -57,20 +52,17 @@ true - com.fasterxml.jackson.core - jackson-databind - true + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + com.fasterxml.jackson.datatype + jackson-datatype-joda + org.springframework.boot spring-boot-starter-test true - - - com.vaadin.external.google - android-json - - io.projectreactor @@ -80,7 +72,7 @@ com.google.protobuf protobuf-java - 3.25.1 + 4.28.3 test @@ -113,12 +105,6 @@ 2.2.0 true - - - org.json - json - 20240303 - @@ -170,6 +156,7 @@ kotlin-maven-plugin org.jetbrains.kotlin + 1.9.25 compile diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/actuator/FunctionsEndpoint.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/actuator/FunctionsEndpoint.java index 008a969db..77682df26 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/actuator/FunctionsEndpoint.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/actuator/FunctionsEndpoint.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.actuator; import java.util.LinkedHashMap; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeMap; @@ -49,33 +50,34 @@ public Map> listAll() { Set names = functionCatalog.getNames(null); for (String name : names) { FunctionInvocationWrapper function = functionCatalog.lookup(name); - Map functionMap = new LinkedHashMap<>(); - if (function.isFunction()) { - functionMap.put("type", "FUNCTION"); - functionMap.put("input-type", this.toSimplePolyIn(function)); - functionMap.put("output-type", this.toSimplePolyOut(function)); + if (function != null) { + Map functionMap = new LinkedHashMap<>(); + if (function.isFunction()) { + functionMap.put("type", "FUNCTION"); + functionMap.put("input-type", this.toSimplePolyIn(function)); + functionMap.put("output-type", this.toSimplePolyOut(function)); + } + else if (function.isConsumer()) { + functionMap.put("type", "CONSUMER"); + functionMap.put("input-type", this.toSimplePolyIn(function)); + } + else { + functionMap.put("type", "SUPPLIER"); + functionMap.put("output-type", this.toSimplePolyOut(function)); + } + allFunctions.put(name, functionMap); } - else if (function.isConsumer()) { - functionMap.put("type", "CONSUMER"); - functionMap.put("input-type", this.toSimplePolyIn(function)); - } - else { - functionMap.put("type", "SUPPLIER"); - functionMap.put("output-type", this.toSimplePolyOut(function)); - } - allFunctions.put(name, functionMap); } - return allFunctions; } private String toSimplePolyOut(FunctionInvocationWrapper function) { - return FunctionTypeUtils.getRawType(function.getItemType(function.getOutputType())).getSimpleName().toLowerCase(); + return FunctionTypeUtils.getRawType(function.getItemType(function.getOutputType())).getSimpleName().toLowerCase(Locale.ROOT); } private String toSimplePolyIn(FunctionInvocationWrapper function) { - return FunctionTypeUtils.getRawType(function.getItemType(function.getInputType())).getSimpleName().toLowerCase(); + return FunctionTypeUtils.getRawType(function.getItemType(function.getInputType())).getSimpleName().toLowerCase(Locale.ROOT); } } 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 af3c81931..90fdbcdbd 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 @@ -17,6 +17,7 @@ package org.springframework.cloud.function.cloudevent; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.Collections; import java.util.HashMap; @@ -170,7 +171,11 @@ private CloudEventMessageUtils() { public static String getId(Message message) { String prefix = determinePrefixToUse(message.getHeaders()); - return (String) message.getHeaders().get(prefix + MessageHeaders.ID); + Object value = message.getHeaders().get(prefix + MessageHeaders.ID); + if (value instanceof byte[] v) { + value = toString(v); + } + return (String) value; } public static URI getSource(Message message) { @@ -180,17 +185,29 @@ public static URI getSource(Message message) { public static String getSpecVersion(Message message) { String prefix = determinePrefixToUse(message.getHeaders()); - return (String) message.getHeaders().get(prefix + _SPECVERSION); + Object value = message.getHeaders().get(prefix + _SPECVERSION); + if (value instanceof byte[] v) { + value = toString(v); + } + return (String) value; } public static String getType(Message message) { String prefix = determinePrefixToUse(message.getHeaders()); - return (String) message.getHeaders().get(prefix + _TYPE); + Object value = message.getHeaders().get(prefix + _TYPE); + if (value instanceof byte[] v) { + value = toString(v); + } + return (String) value; } public static String getDataContentType(Message message) { String prefix = determinePrefixToUse(message.getHeaders()); - return (String) message.getHeaders().get(prefix + _DATACONTENTTYPE); + Object value = message.getHeaders().get(prefix + _DATACONTENTTYPE); + if (value instanceof byte[] v) { + value = toString(v); + } + return (String) value; } public static URI getDataSchema(Message message) { @@ -200,7 +217,11 @@ public static URI getDataSchema(Message message) { public static String getSubject(Message message) { String prefix = determinePrefixToUse(message.getHeaders()); - return (String) message.getHeaders().get(prefix + _SUBJECT); + Object value = message.getHeaders().get(prefix + _SUBJECT); + if (value instanceof byte[] v) { + value = toString(v); + } + return (String) value; } public static OffsetDateTime getTime(Message message) { @@ -434,12 +455,21 @@ private static Message buildBinaryMessageFromStructuredMap(Map map, String key) { Object uri = map.get(key); - if (uri != null && uri instanceof String) { - uri = URI.create((String) uri); + if (uri != null) { + if (uri instanceof String) { + uri = URI.create((String) uri); + } + else if (uri instanceof byte[] u) { + uri = URI.create(toString(u)); + } } return (URI) uri; } + private static String toString(byte[] value) { + return new String(value, StandardCharsets.UTF_8); + } + public static class Protocols { static String AMQP = "amqp"; static String AVRO = "avro"; 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 9d1b6fa2b..136de7949 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 @@ -70,7 +70,7 @@ public class CloudEventsFunctionInvocationHelper implements FunctionInvocationHe } @Override - public boolean isRetainOuputAsMessage(Message message) { + public boolean isRetainOutputAsMessage(Message message) { return message.getHeaders().containsKey(MessageUtils.TARGET_PROTOCOL) || (message.getHeaders().containsKey(MessageUtils.MESSAGE_TYPE) && message.getHeaders().get(MessageUtils.MESSAGE_TYPE).equals(CloudEventMessageUtils.CLOUDEVENT_VALUE)); } 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 efad20fb4..a9e05dbb3 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 @@ -60,6 +60,11 @@ public class FunctionProperties implements EnvironmentAware, ApplicationContextA */ public final static String FUNCTION_DEFINITION = PREFIX + ".definition"; + /** + * Key for the proxy name. + */ + public final static String PROXY = "proxy"; + /** * Definition of the function to be used. This could be function name (e.g., 'myFunction') * or function composition definition (e.g., 'myFunction|yourFunction') 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 6e3b3666b..90a783979 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 @@ -121,7 +121,7 @@ public FunctionRegistration type(Type type) { if (KotlinDetector.isKotlinPresent() && this.target instanceof KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper) { return this; } - Type discoveredFunctionType = FunctionTypeUtils.discoverFunctionTypeFromClass(this.target.getClass()); + Type discoveredFunctionType = type; //FunctionTypeUtils.discoverFunctionTypeFromClass(this.target.getClass()); if (discoveredFunctionType == null) { // only valid for Kafka Stream KStream[] return type. return null; } @@ -146,7 +146,6 @@ else if (outputType == null && inputType != Object.class) { + discoveredFunctionType + "; Provided: " + type); } - return this; } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionTypeProcessor.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionTypeProcessor.java index 67349fc9a..5aae6a037 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionTypeProcessor.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionTypeProcessor.java @@ -31,6 +31,9 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.http.client.ClientHttpRequestFactorySettings; +import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.Ssl.ServerNameSslBundle; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.cloud.function.context.config.FunctionContextUtils; import org.springframework.cloud.function.context.message.MessageUtils; @@ -105,6 +108,15 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati // known static types runtimeHints.reflection().registerType(MessageUtils.MessageStructureWithCaseInsensitiveHeaderKeys.class, MemberCategory.INVOKE_PUBLIC_METHODS); + + + // temporary due to bug in boot + runtimeHints.reflection().registerType(ClientHttpRequestFactorySettings.class, + MemberCategory.INVOKE_PUBLIC_METHODS); + runtimeHints.reflection().registerType(Ssl.class, + MemberCategory.INVOKE_PUBLIC_METHODS); + runtimeHints.reflection().registerType(ServerNameSslBundle.class, + MemberCategory.INVOKE_PUBLIC_METHODS); } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/MessageRoutingCallback.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/MessageRoutingCallback.java index 2ba6000f3..f9b2803ca 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/MessageRoutingCallback.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/MessageRoutingCallback.java @@ -21,28 +21,29 @@ /** * Java-based strategy to assist with determining the name of the route-to function definition. - * Once implementation is registered as a bean in application context + * Once an implementation is registered as a bean in application context * it will be picked up by the {@link RoutingFunction}. - * + *

* While {@link RoutingFunction} provides several mechanisms to determine the route-to function definition * this callback takes precedence over all of them. * * @author Oleg Zhurakousky + * @author John Blum * @since 3.1 */ public interface MessageRoutingCallback { /** - * Computes and returns the instance of {@link FunctionRoutingResult} which encapsulates, - * at the very minimum, function definition. - *

+ * Computes and returns an instance of {@link String}, which encapsulates, + * at the very minimum, a function definition. + *

* Providing such message is primarily an optimization feature. It could be useful for cases * where routing procedure is complex and results in, let's say, conversion of the payload to * the target type, which would effectively be thrown away if the ability to modify the target * message for downstream use didn't exist, resulting in repeated transformation, type conversion etc. * * @param message input message - * @return instance of {@link FunctionRoutingResult} containing the result of the routing computation + * @return instance of {@link String} containing the result of the routing computation */ default String routingResult(Message message) { return (String) message.getHeaders().get(FunctionProperties.FUNCTION_DEFINITION); 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 e62b8a8b6..c5e4ec98f 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 @@ -48,7 +48,6 @@ 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; @@ -121,9 +120,12 @@ public T lookup(Class type, String functionDefinition, String... expected functionDefinition = StringUtils.hasText(functionDefinition) ? functionDefinition : this.applicationContext.getEnvironment().getProperty(FunctionProperties.FUNCTION_DEFINITION, ""); - if (!this.applicationContext.containsBean(functionDefinition) || !KotlinDetector.isKotlinType(this.applicationContext.getBean(functionDefinition).getClass())) { + if (!this.applicationContext.containsBean(functionDefinition) || !KotlinUtils.isKotlinType(this.applicationContext.getBean(functionDefinition))) { functionDefinition = this.normalizeFunctionDefinition(functionDefinition); } + if (!isFunctionDefinitionEligible(functionDefinition)) { + return null; + } if (!StringUtils.hasText(functionDefinition)) { Collection functionalBeans = this.getNames(null).stream() .filter(name -> !RoutingFunction.FUNCTION_NAME.equals(name)) @@ -134,9 +136,7 @@ public T lookup(Class type, String functionDefinition, String... expected + "use 'spring.cloud.function.definition' property to explicitly define it. "); } } - if (!isFunctionDefinitionEligible(functionDefinition)) { - return null; - } + FunctionInvocationWrapper function = this.doLookup(type, functionDefinition, expectedOutputMimeTypes); Object syncInstance = functionDefinition == null ? this : functionDefinition; synchronized (syncInstance) { @@ -144,11 +144,12 @@ public T lookup(Class type, String functionDefinition, String... expected 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"); + if (functionRegistratioinNames.contains(functionName)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping function '" + functionName + "' since it is already present"); + } } else { - Object functionCandidate = this.discoverFunctionInBeanFactory(functionName); if (functionCandidate != null) { Type functionType = null; @@ -159,7 +160,6 @@ public T lookup(Class type, String functionDefinition, String... expected else if (functionCandidate instanceof BiFunction || functionCandidate instanceof BiConsumer) { functionRegistration = this.registerMessagingBiFunction(functionCandidate, functionName); } - //else if (KotlinDetector.isKotlinType(functionCandidate.getClass())) { else if (KotlinUtils.isKotlinType(functionCandidate)) { KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper wrapper = new KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper(functionCandidate); @@ -179,6 +179,10 @@ else if (this.isSpecialFunctionRegistration(functionNames, functionName)) { else { functionType = FunctionTypeUtils.discoverFunctionType(functionCandidate, functionName, this.applicationContext); } + + if (logger.isDebugEnabled()) { + logger.debug("Discovered function type for: " + functionDefinition + " - " + functionType); + } if (functionRegistration == null) { functionRegistration = new FunctionRegistration(functionCandidate, functionName).type(functionType); } 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 d656420d9..8a09989a5 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 @@ -16,8 +16,6 @@ package org.springframework.cloud.function.context.catalog; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; @@ -37,14 +35,15 @@ */ public abstract class FunctionAroundWrapper { - private static final Log log = LogFactory.getLog(FunctionAroundWrapper.class); - public final Object apply(Object input, FunctionInvocationWrapper targetFunction) { + String functionalTracingEnabledStr = System.getProperty("spring.cloud.function.observability.enabled"); boolean functionalTracingEnabled = !StringUtils.hasText(functionalTracingEnabledStr) || Boolean.parseBoolean(functionalTracingEnabledStr); if (functionalTracingEnabled && !(input instanceof Publisher) && input instanceof Message && !FunctionTypeUtils.isCollectionOfMessage(targetFunction.getOutputType())) { - return this.doApply(input, targetFunction); + Object result = this.doApply(input, targetFunction); + targetFunction.wrapped = false; + return result; } else { return targetFunction.apply(input); 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 24e181e12..3fc05f078 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-2025 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. @@ -20,6 +20,8 @@ import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -27,19 +29,31 @@ import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.DoubleConsumer; +import java.util.function.DoubleFunction; +import java.util.function.DoubleSupplier; import java.util.function.Function; +import java.util.function.IntConsumer; +import java.util.function.IntFunction; +import java.util.function.IntSupplier; +import java.util.function.LongConsumer; +import java.util.function.LongFunction; +import java.util.function.LongSupplier; import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; 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; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; @@ -48,11 +62,13 @@ 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.GenericTypeResolver; import org.springframework.core.KotlinDetector; import org.springframework.core.ResolvableType; import org.springframework.messaging.Message; 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; @@ -63,12 +79,15 @@ * * @author Oleg Zhurakousky * @author Andrey Shlykov + * @author Artem Bilan * * @since 3.0 */ public final class FunctionTypeUtils { - private static Log logger = LogFactory.getLog(FunctionTypeUtils.class); + private static Log logger = LogFactory.getLog(FunctionTypeUtils.class); + + private static Type ROUTING_FUNCTION_TYPE = discoverFunctionTypeFromClass(RoutingFunction.class); private FunctionTypeUtils() { @@ -136,7 +155,10 @@ public static Type getGenericType(Type type) { type = getImmediateGenericType(type, 0); } - return TypeResolver.reify(type instanceof GenericArrayType ? type : TypeResolver.reify(type)); + if (type instanceof WildcardType) { + type = Object.class; + } + return type; } /** @@ -145,8 +167,15 @@ public static Type getGenericType(Type type) { * @return instance of {@link Class} as raw representation of the provided {@link Type} */ public static Class getRawType(Type type) { - return type != null ? TypeResolver - .resolveRawClass(type instanceof GenericArrayType ? type : TypeResolver.reify(type), null) : null; + if (type instanceof WildcardType) { + Type[] upperbounds = ((WildcardType) type).getUpperBounds(); + /* + * Kotlin may have something like this which is technically a whildcard yet it has upper/lower types. + * See GH-1260 + */ + return ObjectUtils.isEmpty(upperbounds) ? Object.class : getRawType(upperbounds[0]); + } + return ResolvableType.forType(type).getRawClass(); } /** @@ -160,15 +189,15 @@ public static Class getRawType(Type type) { */ public static Method discoverFunctionalMethod(Class pojoFunctionClass) { if (Supplier.class.isAssignableFrom(pojoFunctionClass)) { - return Stream.of(ReflectionUtils.getDeclaredMethods(pojoFunctionClass)).filter(m -> !m.isSynthetic() + return Stream.of(ReflectionUtils.getAllDeclaredMethods(pojoFunctionClass)).filter(m -> !m.isSynthetic() && m.getName().equals("get")).findFirst().get(); } else if (Consumer.class.isAssignableFrom(pojoFunctionClass) || BiConsumer.class.isAssignableFrom(pojoFunctionClass)) { - return Stream.of(ReflectionUtils.getDeclaredMethods(pojoFunctionClass)).filter(m -> !m.isSynthetic() + return Stream.of(ReflectionUtils.getAllDeclaredMethods(pojoFunctionClass)).filter(m -> !m.isSynthetic() && m.getName().equals("accept")).findFirst().get(); } else if (Function.class.isAssignableFrom(pojoFunctionClass) || BiFunction.class.isAssignableFrom(pojoFunctionClass)) { - return Stream.of(ReflectionUtils.getDeclaredMethods(pojoFunctionClass)).filter(m -> !m.isSynthetic() + return Stream.of(ReflectionUtils.getAllDeclaredMethods(pojoFunctionClass)).filter(m -> !m.isSynthetic() && m.getName().equals("apply")).findFirst().get(); } @@ -182,22 +211,31 @@ else if (Function.class.isAssignableFrom(pojoFunctionClass) || BiFunction.class. !method.getDeclaringClass().isAssignableFrom(Object.class) && !method.isSynthetic() && !method.isBridge() && !method.isVarArgs()); - Assert.isTrue(methods.size() == 1, "Discovered " + methods.size() + " methods that would qualify as 'functional' - " - + methods + ".\n Class '" + pojoFunctionClass + "' is not a FunctionalInterface."); - - return methods.get(0); + if (methods.size() > 1) { + for (Method candidadteMethod : methods) { + if (candidadteMethod.getName().equals("apply") + || candidadteMethod.getName().equals("accept") + || candidadteMethod.getName().equals("get") + || candidadteMethod.getName().equals("invoke")) { + return candidadteMethod; + } + } + } + return CollectionUtils.isEmpty(methods) ? null : methods.get(0); } - @SuppressWarnings("unchecked") public static Type discoverFunctionTypeFromClass(Class functionalClass) { if (KotlinDetector.isKotlinPresent()) { if (Function1.class.isAssignableFrom(functionalClass)) { - return TypeResolver.reify(Function1.class, (Class>) functionalClass); + ResolvableType kotlinType = ResolvableType.forClass(functionalClass).as(Function1.class); + return GenericTypeResolver.resolveType(kotlinType.getType(), functionalClass); } else if (Function0.class.isAssignableFrom(functionalClass)) { - return TypeResolver.reify(Function0.class, (Class>) functionalClass); + ResolvableType kotlinType = ResolvableType.forClass(functionalClass).as(Function0.class); + return GenericTypeResolver.resolveType(kotlinType.getType(), functionalClass); } } + Type typeToReturn = null; if (Function.class.isAssignableFrom(functionalClass)) { for (Type superInterface : functionalClass.getGenericInterfaces()) { if (superInterface != null && !superInterface.equals(Object.class)) { @@ -206,15 +244,18 @@ else if (Function0.class.isAssignableFrom(functionalClass)) { } } } - return TypeResolver.reify(Function.class, (Class>) functionalClass); + ResolvableType functionType = ResolvableType.forClass(functionalClass).as(Function.class); + typeToReturn = GenericTypeResolver.resolveType(functionType.getType(), functionalClass); } else if (Consumer.class.isAssignableFrom(functionalClass)) { - return TypeResolver.reify(Consumer.class, (Class>) functionalClass); + ResolvableType functionType = ResolvableType.forClass(functionalClass).as(Consumer.class); + typeToReturn = GenericTypeResolver.resolveType(functionType.getType(), functionalClass); } else if (Supplier.class.isAssignableFrom(functionalClass)) { - return TypeResolver.reify(Supplier.class, (Class>) functionalClass); + ResolvableType functionType = ResolvableType.forClass(functionalClass).as(Supplier.class); + typeToReturn = GenericTypeResolver.resolveType(functionType.getType(), functionalClass); } - return TypeResolver.reify(functionalClass); + return typeToReturn; } /** @@ -249,26 +290,43 @@ public static Type discoverFunctionTypeFromFunctionFactoryMethod(Method method) * @return type of the function */ public static Type discoverFunctionTypeFromFunctionMethod(Method functionMethod) { + if (functionMethod == null) { + return null; + } Assert.isTrue( functionMethod.getName().equals("apply") || functionMethod.getName().equals("accept") || - functionMethod.getName().equals("get"), + functionMethod.getName().equals("get") || + functionMethod.getName().equals("invoke"), "Only Supplier, Function or Consumer supported at the moment. Was " + functionMethod.getDeclaringClass()); - if (functionMethod.getName().equals("apply")) { - return ResolvableType.forClassWithGenerics(Function.class, - ResolvableType.forMethodParameter(functionMethod, 0), - ResolvableType.forMethodReturnType(functionMethod)).getType(); - + ResolvableType functionType; + if (functionMethod.getName().equals("apply") || functionMethod.getName().equals("invoke")) { + ResolvableType input = ResolvableType.forMethodParameter(functionMethod, 0); + if (input.getType() instanceof TypeVariable) { + input = ResolvableType.forClass(Object.class); + } + ResolvableType output = ResolvableType.forMethodReturnType(functionMethod); + if (output.getType() instanceof TypeVariable) { + output = ResolvableType.forClass(Object.class); + } + functionType = ResolvableType.forClassWithGenerics(Function.class, input, output); } else if (functionMethod.getName().equals("accept")) { - return ResolvableType.forClassWithGenerics(Consumer.class, - ResolvableType.forMethodParameter(functionMethod, 0)).getType(); + ResolvableType parameterType = ResolvableType.forMethodParameter(functionMethod, 0); + if (parameterType.getType() instanceof TypeVariable) { + parameterType = ResolvableType.forClass(Object.class); + } + functionType = ResolvableType.forClassWithGenerics(Consumer.class, parameterType); } else { - return ResolvableType.forClassWithGenerics(Supplier.class, - ResolvableType.forMethodReturnType(functionMethod)).getType(); + ResolvableType returnType = ResolvableType.forMethodReturnType(functionMethod); + if (returnType.getType() instanceof TypeVariable) { + returnType = ResolvableType.forClass(Object.class); + } + functionType = ResolvableType.forClassWithGenerics(Supplier.class, returnType); } + return functionType.getType(); } public static int getInputCount(FunctionInvocationWrapper function) { @@ -318,32 +376,46 @@ public static Type getComponentTypeOfOutputType(Type functionType) { * @param functionType the Type of Function or Consumer * @return the input type as {@link Type} */ - @SuppressWarnings("unchecked") public static Type getInputType(Type functionType) { + assertSupportedTypes(functionType); if (isSupplier(functionType)) { logger.debug("Supplier does not have input type, returning null as input type."); return null; } - assertSupportedTypes(functionType); - Type inputType; - if (functionType instanceof Class) { - functionType = Function.class.isAssignableFrom((Class) functionType) - ? TypeResolver.reify(Function.class, (Class>) functionType) - : TypeResolver.reify(Consumer.class, (Class>) functionType); - } + ResolvableType resolvableFunctionType = ResolvableType.forType(functionType); - inputType = functionType instanceof ParameterizedType - ? ((ParameterizedType) functionType).getActualTypeArguments()[0] - : Object.class; + ResolvableType resolvableInputType = resolvableFunctionType.as(resolvableFunctionType.getRawClass()); - return inputType; + if (resolvableInputType.getType() instanceof ParameterizedType) { + return resolvableInputType.getGeneric(0).getType(); + } + else { + // will try another way. See GH-1251 + if (FunctionTypeUtils.isFunction(functionType)) { + resolvableInputType = resolvableFunctionType.as(Function.class); + } + else { + if (KotlinDetector.isKotlinPresent() && Function1.class.isAssignableFrom(getRawType(functionType))) { // Kotlin + return ResolvableType.forType(getImmediateGenericType(functionType, 1)).getType(); + } + else { + resolvableInputType = resolvableFunctionType.as(Consumer.class); + } + } + if (resolvableInputType.getType() instanceof ParameterizedType) { + return resolvableInputType.getGeneric(0).getType(); + } + else { + return Object.class; + } + } } @SuppressWarnings("rawtypes") public static Type discoverFunctionType(Object function, String functionName, GenericApplicationContext applicationContext) { if (function instanceof RoutingFunction) { - return FunctionContextUtils.findType(applicationContext.getBeanFactory(), functionName); + return ROUTING_FUNCTION_TYPE; } else if (function instanceof FunctionRegistration) { return ((FunctionRegistration) function).getType(); @@ -354,58 +426,95 @@ else if (function instanceof FunctionRegistration) { return fr.getType(); } - boolean beanDefinitionExists = false; - String functionBeanDefinitionName = discoverDefinitionName(functionName, applicationContext); - beanDefinitionExists = applicationContext.getBeanFactory().containsBeanDefinition(functionBeanDefinitionName); - if (applicationContext.containsBean("&" + functionName)) { - Class objectType = applicationContext.getBean("&" + functionName, FactoryBean.class) - .getObjectType(); - return FunctionTypeUtils.discoverFunctionTypeFromClass(objectType); - } + functionName = discoverBeanDefinitionNameByQualifier(applicationContext.getBeanFactory(), functionName); + Type type = FunctionContextUtils.findType(applicationContext.getBeanFactory(), functionName); + if (type == null || type instanceof Class) { + boolean beanDefinitionExists = false; + String functionBeanDefinitionName = discoverDefinitionName(functionName, applicationContext); + beanDefinitionExists = applicationContext.getBeanFactory().containsBeanDefinition(functionBeanDefinitionName); + if (applicationContext.containsBean("&" + functionName)) { + Class objectType = applicationContext.getBean("&" + functionName, FactoryBean.class) + .getObjectType(); + return FunctionTypeUtils.discoverFunctionTypeFromClass(objectType); + } - Type type = FunctionTypeUtils.discoverFunctionTypeFromClass(function.getClass()); - if (beanDefinitionExists) { - Type t = FunctionTypeUtils.getImmediateGenericType(type, 0); - if (t == null || t == Object.class) { - type = FunctionContextUtils.findType(applicationContext.getBeanFactory(), functionBeanDefinitionName); + type = FunctionTypeUtils.discoverFunctionTypeFromClass(function.getClass()); + if (beanDefinitionExists) { + Type t = FunctionTypeUtils.getImmediateGenericType(type, 0); + if (t == null || t == Object.class) { + type = FunctionContextUtils.findType(applicationContext.getBeanFactory(), functionBeanDefinitionName); + } + } + else if (!(type instanceof ParameterizedType)) { + String beanDefinitionName = discoverBeanDefinitionNameByQualifier(applicationContext.getBeanFactory(), functionName); + if (StringUtils.hasText(beanDefinitionName)) { + type = FunctionContextUtils.findType(applicationContext.getBeanFactory(), beanDefinitionName); + } } } - else if (!(type instanceof ParameterizedType)) { - String beanDefinitionName = discoverBeanDefinitionNameByQualifier(applicationContext.getBeanFactory(), functionName); - if (StringUtils.hasText(beanDefinitionName)) { - type = FunctionContextUtils.findType(applicationContext.getBeanFactory(), beanDefinitionName); + else if (type instanceof ParameterizedType) { + ResolvableType resolvableType = ResolvableType.forType(type); + if (FactoryBean.class.isAssignableFrom(resolvableType.toClass())) { + return resolvableType.getGeneric(0).getType(); } } return type; } public static String discoverBeanDefinitionNameByQualifier(ListableBeanFactory beanFactory, String qualifier) { - Map beanMap = BeanFactoryAnnotationUtils.qualifiedBeansOfType(beanFactory, Object.class, qualifier); - if (!CollectionUtils.isEmpty(beanMap) && beanMap.size() == 1) { - return beanMap.keySet().iterator().next(); + String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, Object.class); + + for (String beanName : candidateBeans) { + if (BeanFactoryAnnotationUtils.isQualifierMatch(qualifier::equals, beanName, beanFactory)) { + return beanName; + } } return null; } - @SuppressWarnings("unchecked") public static Type getOutputType(Type functionType) { assertSupportedTypes(functionType); if (isConsumer(functionType)) { logger.debug("Consumer does not have output type, returning null as output type."); return null; } - Type outputType; - if (functionType instanceof Class) { - functionType = Function.class.isAssignableFrom((Class) functionType) - ? TypeResolver.reify(Function.class, (Class>) functionType) - : TypeResolver.reify(Supplier.class, (Class>) functionType); - } - outputType = functionType instanceof ParameterizedType - ? (isSupplier(functionType) ? ((ParameterizedType) functionType).getActualTypeArguments()[0] : ((ParameterizedType) functionType).getActualTypeArguments()[1]) - : Object.class; + ResolvableType resolvableFunctionType = ResolvableType.forType(functionType); + + ResolvableType resolvableOutputType; + if (FunctionTypeUtils.isFunction(functionType)) { + resolvableOutputType = resolvableFunctionType.as(Function.class); + } + else { + if (KotlinDetector.isKotlinPresent() && Function1.class.isAssignableFrom(getRawType(functionType))) { // Kotlin + return ResolvableType.forType(getImmediateGenericType(functionType, 1)).getType(); + } + else { + resolvableOutputType = resolvableFunctionType.as(Supplier.class); + } + } - return outputType; + Type outputType; + if (functionType instanceof Class functionTypeClass) { + if (FunctionTypeUtils.isFunction(functionType)) { + ResolvableType genericClass1 = resolvableOutputType.getGeneric(1); + outputType = genericClass1.getType(); + outputType = (outputType instanceof TypeVariable) ? Object.class : GenericTypeResolver.resolveType(outputType, functionTypeClass); + } + else { + ResolvableType genericClass0 = resolvableOutputType.getGeneric(0); + outputType = genericClass0.getType(); + outputType = (outputType instanceof TypeVariable) ? Object.class : GenericTypeResolver.resolveType(outputType, functionTypeClass); + } + } + else if (functionType instanceof ParameterizedType) { + Type genericType = isSupplier(functionType) ? resolvableOutputType.getGeneric(0).getType() : resolvableOutputType.getGeneric(1).getType(); + outputType = GenericTypeResolver.resolveType(genericType, getRawType(functionType)); + } + else { + outputType = resolvableOutputType.getType(); + } + return outputType instanceof TypeVariable ? Object.class : outputType; } public static Type getImmediateGenericType(Type type, int index) { @@ -420,7 +529,7 @@ public static boolean isPublisher(Type type) { } public static boolean isFlux(Type type) { - return TypeResolver.resolveRawClass(type, null) == Flux.class; + return getRawType(type) == Flux.class; } public static boolean isCollectionOfMessage(Type type) { @@ -475,10 +584,10 @@ public static boolean isMono(Type type) { public static boolean isMultipleArgumentType(Type type) { if (type != null) { - if (TypeResolver.resolveRawClass(type, null).isArray()) { + if (ResolvableType.forType(type).isArray()) { return false; } - Class clazz = TypeResolver.resolveRawClass(TypeResolver.reify(type), null); + Class clazz = ResolvableType.forType(type).getRawClass(); return clazz.getName().startsWith("reactor.util.function.Tuple"); } return false; @@ -534,10 +643,24 @@ private static void assertSupportedTypes(Type type) { Class candidateType = (Class) type; Assert.isTrue(Supplier.class.isAssignableFrom(candidateType) - || Function.class.isAssignableFrom(candidateType) - || Consumer.class.isAssignableFrom(candidateType) - || FunctionRegistration.class.isAssignableFrom(candidateType) - || type.getTypeName().startsWith("org.springframework.context.annotation.ConfigurationClassEnhancer"), "Must be one of Supplier, Function, Consumer" + || (KotlinDetector.isKotlinPresent() && (Function0.class.isAssignableFrom(candidateType) || Function1.class.isAssignableFrom(candidateType))) + || Function.class.isAssignableFrom(candidateType) + || Consumer.class.isAssignableFrom(candidateType) + || FunctionRegistration.class.isAssignableFrom(candidateType) + || IntConsumer.class.isAssignableFrom(candidateType) + || IntSupplier.class.isAssignableFrom(candidateType) + || IntFunction.class.isAssignableFrom(candidateType) + || ToIntFunction.class.isAssignableFrom(candidateType) + || LongConsumer.class.isAssignableFrom(candidateType) + || LongSupplier.class.isAssignableFrom(candidateType) + || LongFunction.class.isAssignableFrom(candidateType) + || ToLongFunction.class.isAssignableFrom(candidateType) + || DoubleConsumer.class.isAssignableFrom(candidateType) + || DoubleSupplier.class.isAssignableFrom(candidateType) + || DoubleFunction.class.isAssignableFrom(candidateType) + || ToDoubleFunction.class.isAssignableFrom(candidateType) + || type.getTypeName().startsWith("org.springframework.context.annotation.ConfigurationClassEnhancer"), + "Must be one of Supplier, Function, Consumer" + " or FunctionRegistration. Was " + type); } 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 3c76d0bcf..cbde568cb 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 @@ -21,10 +21,12 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -183,11 +185,10 @@ else if (registration.getNames().contains(definition) || registration.getTarget( boolean isFunctionDefinitionEligible(String functionDefinition) { if (this.functionProperties != null) { - for (String definition : this.functionProperties.getIneligibleDefinitions()) { - if (functionDefinition.contains(definition)) { - return false; - } - } + this.functionProperties.getIneligibleDefinitions().contains(functionDefinition); + boolean matchFoundInBoth = !Collections.disjoint(Arrays.asList(functionDefinition.split("\\|")), + this.functionProperties.getIneligibleDefinitions()); + return !matchFoundInBoth; } return true; } @@ -299,6 +300,8 @@ private FunctionInvocationWrapper compose(Class type, String functionDefiniti for (String functionName : functionNames) { FunctionInvocationWrapper function = this.findFunctionInFunctionRegistrations(functionName); if (function == null) { + logger.warn("Failed to locate function '" + functionName + "' for function definition '" + + functionDefinition + "'. Returning null."); return null; } else { @@ -394,7 +397,7 @@ private FunctionInvocationWrapper invocationWrapperInstance(String functionDefin @SuppressWarnings("rawtypes") public class FunctionInvocationWrapper implements Function, Consumer, Supplier, Runnable { - private final Object target; + private Object target; private Type inputType; @@ -416,12 +419,14 @@ public class FunctionInvocationWrapper implements Function, Cons private boolean propagateInputHeaders; - private boolean wrapped; + protected boolean wrapped; private final ThreadLocal> unconvertedResult = new ThreadLocal<>(); private PostProcessingFunction postProcessor; + private Consumer skipInputConversionCallback; + /* * This is primarily to support Stream's ability to access * un-converted payload (e.g., to evaluate expression on some attribute of a payload) @@ -483,6 +488,9 @@ public boolean isSkipOutputConversion() { return skipOutputConversion; } + public boolean isSkipInputConversion() { + return skipInputConversion; + } public boolean isPrototype() { return !this.isSingleton; @@ -493,6 +501,13 @@ public void setSkipInputConversion(boolean skipInputConversion) { logger.debug("'skipInputConversion' was explicitely set to true. No input conversion will be attempted"); } this.skipInputConversion = skipInputConversion; + if (this.skipInputConversionCallback != null) { + this.skipInputConversionCallback.accept(skipInputConversion); + } + } + + void setSkipInputConversionCallback(Consumer skipInputConversionCallback) { + this.skipInputConversionCallback = skipInputConversionCallback; } public void setSkipOutputConversion(boolean skipOutputConversion) { @@ -548,9 +563,6 @@ public Type getItemType(Type type) { if (FunctionTypeUtils.isPublisher(type) || FunctionTypeUtils.isMessage(type) || FunctionTypeUtils.isTypeCollection(type)) { type = FunctionTypeUtils.getGenericType(type); } - if (FunctionTypeUtils.isMessage(type)) { - type = FunctionTypeUtils.getGenericType(type); - } return type; } @@ -646,13 +658,21 @@ public Function andThen(Function aft || FunctionTypeUtils.isMultipleArgumentType(((FunctionInvocationWrapper) after).outputType)) { throw new UnsupportedOperationException("Composition of functions with multiple arguments is not supported at the moment"); } + FunctionInvocationWrapper afterWrapper = (FunctionInvocationWrapper) after; + + //see GH-1141 for this code snippet + if ((this.getTarget() instanceof Supplier || this.getTarget() instanceof Function) && FunctionTypeUtils.isPublisher(this.getOutputType()) + && afterWrapper.getTarget() instanceof Consumer && !FunctionTypeUtils.isPublisher(afterWrapper.getInputType())) { + Consumer wrapper = new ConsumerWrapper((Consumer) afterWrapper.getTarget()); + afterWrapper.target = wrapper; + afterWrapper.inputType = this.outputType; + } + // this.setSkipOutputConversion(true); ((FunctionInvocationWrapper) after).setSkipOutputConversion(true); Function rawComposedFunction = v -> ((FunctionInvocationWrapper) after).doApply(doApply(v)); - FunctionInvocationWrapper afterWrapper = (FunctionInvocationWrapper) after; - Type composedFunctionType; if (afterWrapper.outputType == null) { composedFunctionType = (this.inputType == null) ? @@ -684,6 +704,10 @@ else if (this.outputType == null) { String composedName = this.functionDefinition + "|" + afterWrapper.functionDefinition; FunctionInvocationWrapper composedFunction = invocationWrapperInstance(composedName, rawComposedFunction, composedFunctionType); + composedFunction.setSkipInputConversionCallback((skipInputConversion) -> { + this.setSkipInputConversion(skipInputConversion); + afterWrapper.setSkipInputConversion(skipInputConversion); + }); composedFunction.composed = true; if (((FunctionInvocationWrapper) after).target instanceof PostProcessingFunction) { composedFunction.postProcessor = (PostProcessingFunction) ((FunctionInvocationWrapper) after).target; @@ -781,18 +805,11 @@ private Class getRawClassFor(@Nullable Type type) { */ 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); - } + if (functionInvocationHelper != null && CloudEventMessageUtils.isCloudEvent(((Message) input))) { + result = functionInvocationHelper.postProcessResult(result, (Message) input); } - else { - if (functionInvocationHelper != null && CloudEventMessageUtils.isCloudEvent(((Message) input))) { - result = functionInvocationHelper.postProcessResult(result, (Message) input); - } - else if (!FunctionTypeUtils.isCollectionOfMessage(this.outputType)) { - result = MessageBuilder.withPayload(result).copyHeaders(this.sanitizeHeaders(((Message) input).getHeaders())).build(); - } + if (!(result instanceof Message) && !FunctionTypeUtils.isCollectionOfMessage(this.outputType)) { + result = MessageBuilder.withPayload(result).copyHeaders(this.sanitizeHeaders(((Message) input).getHeaders())).build(); } } return result; @@ -836,7 +853,7 @@ private Object fluxifyInputIfNecessary(Object input) { if ((!treatPayloadAsPlainText && JsonMapper.isJsonStringRepresentsCollection(payload)) && !FunctionTypeUtils.isTypeCollection(this.inputType) && !FunctionTypeUtils.isTypeArray(this.inputType)) { - MessageHeaders headers = ((Message) input).getHeaders(); + MessageHeaders headers = input instanceof Message ? ((Message) input).getHeaders() : new MessageHeaders(Collections.emptyMap()); Collection collectionPayload = jsonMapper.fromJson(payload, Collection.class); Class inputClass = FunctionTypeUtils.getRawType(this.inputType); if (this.isInputTypeMessage()) { @@ -884,7 +901,7 @@ private String contentTypeHeaderValue(Message msg) { if (contentType == null) { contentType = msg.getHeaders().get(HttpHeaders.CONTENT_TYPE); if (contentType == null) { - contentType = msg.getHeaders().get(HttpHeaders.CONTENT_TYPE.toLowerCase()); + contentType = msg.getHeaders().get(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT)); } } return Objects.toString(contentType); @@ -893,21 +910,21 @@ private String contentTypeHeaderValue(Message msg) { @SuppressWarnings("unchecked") private Object invokeFunction(Object convertedInput) { Object result; - if (!this.isTypePublisher(this.inputType) && convertedInput instanceof Publisher) { - result = convertedInput instanceof Mono - ? Mono.from((Publisher) convertedInput).map(value -> this.invokeFunctionAndEnrichResultIfNecessary(value)) + if (!this.isTypePublisher(this.inputType) && convertedInput instanceof Publisher publisherInput) { + result = publisherInput instanceof Mono + ? Mono.from(publisherInput).map(value -> this.invokeFunctionAndEnrichResultIfNecessary(value)) .doOnError(ex -> logger.error("Failed to invoke function '" + this.functionDefinition + "'", (Throwable) ex)) - : Flux.from((Publisher) convertedInput).map(value -> this.invokeFunctionAndEnrichResultIfNecessary(value)) + : Flux.from(publisherInput).map(value -> this.invokeFunctionAndEnrichResultIfNecessary(value)) .doOnError(ex -> logger.error("Failed to invoke function '" + this.functionDefinition + "'", (Throwable) ex)); } else { result = this.invokeFunctionAndEnrichResultIfNecessary(convertedInput); - if (result instanceof Flux) { - result = ((Flux) result).doOnError(ex -> logger.error("Failed to invoke function '" + if (result instanceof Flux flux) { + result = flux.doOnError(ex -> logger.error("Failed to invoke function '" + this.functionDefinition + "'", (Throwable) ex)); } - else if (result instanceof Mono) { - result = ((Mono) result).doOnError(ex -> logger.error("Failed to invoke function '" + else if (result instanceof Mono mono) { + result = mono.doOnError(ex -> logger.error("Failed to invoke function '" + this.functionDefinition + "'", (Throwable) ex)); } } @@ -962,8 +979,8 @@ else if (value instanceof Mono) { result = this.postProcessFunction((Publisher) result, firstInputMessage); } - return value instanceof OriginalMessageHolder - ? this.enrichInvocationResultIfNecessary(((OriginalMessageHolder) value).getOriginalMessage(), result) + return value instanceof OriginalMessageHolder originalMessageHolder + ? this.enrichInvocationResultIfNecessary((originalMessageHolder).getOriginalMessage(), result) : result; } @@ -1008,8 +1025,8 @@ private Publisher postProcessFunction(Publisher result, AtomicReference { flux = Flux.from((Publisher) flux).map(v -> this.extractValueFromOriginalValueHolderIfNecessary(v)); ((Consumer) this.target).accept(flux); @@ -1025,12 +1042,12 @@ private Object invokeConsumer(Object convertedInput) { }).then(); } } - else if (convertedInput instanceof Publisher) { + else if (convertedInput instanceof Publisher publisherInput) { result = convertedInput instanceof Mono - ? Mono.from((Publisher) convertedInput) + ? Mono.from(publisherInput) .map(v -> this.extractValueFromOriginalValueHolderIfNecessary(v)) .doOnNext((Consumer) this.target).then() - : Flux.from((Publisher) convertedInput) + : Flux.from(publisherInput) .map(v -> this.extractValueFromOriginalValueHolderIfNecessary(v)) .doOnNext((Consumer) this.target).then(); } @@ -1103,6 +1120,9 @@ else if (FunctionTypeUtils.isMultipleArgumentType(type)) { convertedInput = Tuples.fromArray(convertedInputs); } else if (this.skipInputConversion) { + if (!(input instanceof Message)) { + input = MessageBuilder.withPayload(input).build(); + } convertedInput = this.isInputTypeMessage() ? input : new OriginalMessageHolder(((Message) input).getPayload(), (Message) input); @@ -1155,18 +1175,12 @@ private Message filterOutHeaders(Message message) { } private boolean isExtractPayload(Message message, Type type) { - if (this.propagateInputHeaders) { - return false; - } - if (this.isRoutingFunction()) { + if (this.propagateInputHeaders || this.isRoutingFunction() || FunctionTypeUtils.isMessage(type)) { return false; } if (FunctionTypeUtils.isCollectionOfMessage(type)) { return true; } - if (FunctionTypeUtils.isMessage(type)) { - return false; - } Object payload = message.getPayload(); if ((payload instanceof byte[])) { @@ -1261,7 +1275,7 @@ else if (ObjectUtils.isArray(convertedOutput) && !(convertedOutput instanceof by * case that requires it since it may contain forwarding url */ private boolean containsRetainMessageSignalInHeaders(Message message) { - if (functionInvocationHelper != null && functionInvocationHelper.isRetainOuputAsMessage(message)) { + if (functionInvocationHelper != null && functionInvocationHelper.isRetainOutputAsMessage(message)) { return true; } else { @@ -1318,7 +1332,7 @@ private boolean isWrapConvertedInputInMessage(Object convertedInput) { * */ private Type extractActualValueTypeIfNecessary(Type type) { - if (type instanceof ParameterizedType && (FunctionTypeUtils.isPublisher(type) || FunctionTypeUtils.isMessage(type))) { + if (type instanceof ParameterizedType && (FunctionTypeUtils.isPublisher(type) || FunctionTypeUtils.isMessage(type))) { return FunctionTypeUtils.getGenericType(type); } return type; @@ -1343,6 +1357,12 @@ private Object convertInputMessageIfNecessary(Message message, Type type) { if (collectionType == itemType) { return message.getPayload(); } + + if (collectionType != null + && FunctionTypeUtils.getRawType(itemType).isAssignableFrom(collectionType.getClass()) + && FunctionTypeUtils.isMessage(type)) { + return message; + } } Object convertedInput = message.getPayload(); @@ -1405,9 +1425,15 @@ private Object convertMultipleOutputArgumentTypeIfNecesary(Object output, Type t */ @SuppressWarnings("unchecked") private Object convertOutputMessageIfNecessary(Object output, String expectedOutputContetntType) { - String contentType = ((Message) output).getHeaders().containsKey(FunctionProperties.EXPECT_CONTENT_TYPE_HEADER) - ? (String) ((Message) output).getHeaders().get(FunctionProperties.EXPECT_CONTENT_TYPE_HEADER) - : expectedOutputContetntType; + String contentType; + if (this.isOutputTypeMessage() && ((Message) output).getHeaders().containsKey(MessageHeaders.CONTENT_TYPE)) { + contentType = ((Message) output).getHeaders().get(MessageHeaders.CONTENT_TYPE).toString(); + } + else { + 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()); @@ -1526,4 +1552,20 @@ public Object apply(Object t) { return t; } } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static class ConsumerWrapper implements Consumer> { + + private final Consumer targetConsumer; + + ConsumerWrapper(Consumer targetConsumer) { + this.targetConsumer = targetConsumer; + } + + @Override + public void accept(Flux messageFlux) { + messageFlux.doOnNext(this.targetConsumer).subscribe(); + } + + } } 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 1d0b50871..6a3eeee2c 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 @@ -25,11 +25,17 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.gson.Gson; import io.cloudevents.spring.messaging.CloudEventMessageConverter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -49,7 +55,6 @@ import org.springframework.cloud.function.json.GsonMapper; import org.springframework.cloud.function.json.JacksonMapper; import org.springframework.cloud.function.json.JsonMapper; -import org.springframework.cloud.function.utils.PrimitiveTypesFromStringMessageConverter; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; @@ -58,19 +63,24 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.context.expression.BeanFactoryResolver; +import org.springframework.core.KotlinDetector; 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; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter; +import org.springframework.messaging.converter.ContentTypeResolver; 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.InvalidMimeTypeException; +import org.springframework.util.MimeType; import org.springframework.util.StringUtils; /** @@ -83,11 +93,11 @@ * @author Chris Bono */ @Configuration(proxyBeanMethods = false) -@ConditionalOnMissingBean(FunctionCatalog.class) @EnableConfigurationProperties(FunctionProperties.class) @AutoConfigureAfter(name = {"org.springframework.cloud.function.deployer.FunctionDeployerConfiguration"}) public class ContextFunctionCatalogAutoConfiguration { + private static Log logger = LogFactory.getLog(ContextFunctionCatalogAutoConfiguration.class); /** * The name of the property to specify desired JSON mapper. Available values are `jackson' and 'gson'. */ @@ -126,10 +136,27 @@ public FunctionRegistry functionCatalog(List messageConverters mcList.add(new JsonMessageConverter(jsonMapper)); mcList.add(new ByteArrayMessageConverter()); - mcList.add(new StringMessageConverter()); - mcList.add(new PrimitiveTypesFromStringMessageConverter(conversionService)); + StringMessageConverter stringConverter = new StringMessageConverter(); + stringConverter.setSerializedPayloadClass(String.class); + stringConverter.setContentTypeResolver(new ContentTypeResolver() { + @Override + public MimeType resolve(MessageHeaders headers) throws InvalidMimeTypeException { + if (headers.containsKey(MessageHeaders.CONTENT_TYPE)) { + if (headers.get(MessageHeaders.CONTENT_TYPE).toString().startsWith("text")) { + return MimeType.valueOf("text/plain"); + } + else { + return MimeType.valueOf(headers.get(MessageHeaders.CONTENT_TYPE).toString()); + } + } + return null; + } + }); + mcList.add(stringConverter); - messageConverter = new SmartCompositeMessageConverter(mcList); + messageConverter = new SmartCompositeMessageConverter(mcList, () -> { + return context.getBeansOfType(MessageConverterHelper.class).values(); + }); if (functionInvocationHelper instanceof CloudEventsFunctionInvocationHelper) { ((CloudEventsFunctionInvocationHelper) functionInvocationHelper).setMessageConverter(messageConverter); } @@ -181,6 +208,7 @@ protected static class PlainFunctionScanConfiguration { @Configuration(proxyBeanMethods = false) public static class JsonMapperConfiguration { @Bean + @ConditionalOnMissingBean(JsonMapper.class) public JsonMapper jsonMapper(ApplicationContext context) { String preferredMapper = context.getEnvironment().getProperty(JSON_MAPPER_PROPERTY); if (StringUtils.hasText(preferredMapper)) { @@ -213,17 +241,71 @@ private JsonMapper gson(ApplicationContext context) { return new GsonMapper(gson); } + @SuppressWarnings("unchecked") private JsonMapper jackson(ApplicationContext context) { ObjectMapper mapper; try { - mapper = context.getBean(ObjectMapper.class); + mapper = context.getBean(ObjectMapper.class).copy(); } catch (Exception e) { mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + } + mapper.registerModule(new JodaModule()); + if (KotlinDetector.isKotlinPresent()) { + try { + if (!mapper.getRegisteredModuleIds().contains("com.fasterxml.jackson.module.kotlin.KotlinModule")) { + Class kotlinModuleClass = (Class) + ClassUtils.forName("com.fasterxml.jackson.module.kotlin.KotlinModule", ClassUtils.getDefaultClassLoader()); + Module kotlinModule = BeanUtils.instantiateClass(kotlinModuleClass); + mapper.registerModule(kotlinModule); + } + } + catch (ClassNotFoundException ex) { + // jackson-module-kotlin not available + } } mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); - mapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true); +// mapper.configure(DeserializationFeature.FAIL_ON_TRAILING_TOKENS, true); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + if (logger.isDebugEnabled()) { + logger.debug("ObjectMapper configuration: " + getConfigDetails(mapper)); + } return new JacksonMapper(mapper); } + + private static String getConfigDetails(ObjectMapper mapper) { + StringBuilder sb = new StringBuilder(); + + sb.append("Modules:\n"); + if (mapper.getRegisteredModuleIds().isEmpty()) { + sb.append("\t").append("-none-").append("\n"); + } + for (Object m : mapper.getRegisteredModuleIds()) { + sb.append(" ").append(m).append("\n"); + } + + sb.append("\nSerialization Features:\n"); + for (SerializationFeature f : SerializationFeature.values()) { + sb.append("\t").append(f).append(" -> ") + .append(mapper.getSerializationConfig().hasSerializationFeatures(f.getMask())); + if (f.enabledByDefault()) { + sb.append(" (enabled by default)"); + } + sb.append("\n"); + } + + sb.append("\nDeserialization Features:\n"); + for (DeserializationFeature f : DeserializationFeature.values()) { + sb.append("\t").append(f).append(" -> ") + .append(mapper.getDeserializationConfig().hasDeserializationFeatures(f.getMask())); + if (f.enabledByDefault()) { + sb.append(" (enabled by default)"); + } + sb.append("\n"); + } + + return sb.toString(); + } } } 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 46a501436..dacf7b74a 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 @@ -27,6 +27,8 @@ 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.context.annotation.Bean; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.Resource; import org.springframework.core.type.MethodMetadata; import org.springframework.util.ClassUtils; @@ -86,11 +88,11 @@ else if (source instanceof Resource) { } public static Class[] getParamTypesFromBeanDefinitionFactory(Class factory, - AbstractBeanDefinition definition) { + AbstractBeanDefinition definition, String methodName) { if (definition instanceof RootBeanDefinition) { RootBeanDefinition root = (RootBeanDefinition) definition; for (Method method : getCandidateMethods(factory, root)) { - if (root.isFactoryMethod(method)) { + if (method.getName().equals(methodName) && AnnotationUtils.findAnnotation(method, Bean.class) != null) { return method.getParameterTypes(); } } @@ -114,7 +116,7 @@ private static Class resolveBeanClass(AbstractBeanDefinition beanDefinition) private static Type findBeanType(AbstractBeanDefinition definition, String declaringClassName, String methodName) { Class factory = ClassUtils.resolveClassName(declaringClassName, null); - Class[] params = getParamTypesFromBeanDefinitionFactory(factory, definition); + Class[] params = getParamTypesFromBeanDefinitionFactory(factory, definition, methodName); Method method = ReflectionUtils.findMethod(factory, methodName, params); Type type = method.getGenericReturnType(); 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 414e39ecc..99721c184 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 @@ -128,6 +128,9 @@ else if (logger.isDebugEnabled()) { @Override protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { + if (payload.getClass().getName().equals("org.springframework.kafka.support.KafkaNull")) { + return payload; + } return jsonMapper.toJson(payload); } 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 034a6b883..e40aefd8f 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 @@ -22,7 +22,6 @@ import java.util.function.Function; import java.util.function.Supplier; -import com.fasterxml.jackson.module.kotlin.KotlinModule; import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; @@ -37,14 +36,10 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 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; -import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.util.ObjectUtils; /** @@ -56,25 +51,12 @@ * @author Dmitriy Tsypov * @since 2.0 */ -@Configuration +@Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "kotlin.jvm.functions.Function0") public class KotlinLambdaToFunctionAutoConfiguration { protected final Log logger = LogFactory.getLog(getClass()); - @Bean - @ConditionalOnMissingBean - @ConditionalOnClass(name = {"org.springframework.http.converter.json.Jackson2ObjectMapperBuilder", - "com.fasterxml.jackson.module.kotlin.KotlinModule"}) - Jackson2ObjectMapperBuilderCustomizer customizer() { - return new Jackson2ObjectMapperBuilderCustomizer() { - @Override - public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) { - jacksonObjectMapperBuilder.modulesToInstall(KotlinModule.class); - } - }; - } - @SuppressWarnings({ "unchecked", "rawtypes" }) public static final class KotlinFunctionWrapper implements Function, Supplier, Consumer, @@ -123,7 +105,11 @@ public Object invoke(Object arg0) { if (this.kotlinLambdaTarget instanceof Function1) { return ((Function1) this.kotlinLambdaTarget).invoke(arg0); } - return ((Function) this.kotlinLambdaTarget).apply(arg0); + else if (this.kotlinLambdaTarget instanceof Function) { + return ((Function) this.kotlinLambdaTarget).apply(arg0); + } + ((Consumer) this.kotlinLambdaTarget).accept(arg0); + return null; } @Override diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/MessageConverterHelper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/MessageConverterHelper.java new file mode 100644 index 000000000..759fd1016 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/MessageConverterHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2025 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 org.springframework.messaging.Message; + +/** + * @author Oleg Zhurakousky + */ +public interface MessageConverterHelper { + + /** + * This method will be called by the framework in cases when a message failed to convert. + * It allows you to signal to the framework if such failure should be considered fatal or not. + * + * @param message failed message + * @return true if conversion failure must be considered fatal. + */ + default boolean shouldFailIfCantConvert(Message message) { + return false; + } + + + /** + * This method will be called by the framework in cases when a message failed to convert. + * It allows you to signal to the framework if such failure should be considered fatal or not. + * + * @param message failed message + * @param t exception (coudl be null) + * @return true if conversion failure must be considered fatal. + */ + default boolean shouldFailIfCantConvert(Message message, Throwable t) { + if (t == null) { + return this.shouldFailIfCantConvert(message); + } + return false; + } + + /** + * This method will be called by the framework in cases when a single message within batch of messages failed to convert. + * It provides a place for providing post-processing logic before message converter returns. + * + * @param message failed message. + * @param index index of failed message within the batch + */ + default void postProcessBatchMessageOnFailure(Message message, int index) { + } +} 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 53fe2736a..8aecd9500 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,9 +16,11 @@ package org.springframework.cloud.function.context.config; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -43,10 +45,11 @@ import org.springframework.util.StringUtils; /** - * An implementation of Function which acts as a gateway/router by actually + * An implementation of {@link Function} which acts as a gateway/router by actually * delegating incoming invocation to a function specified .. . * * @author Oleg Zhurakousky + * @author John Blum * @since 2.1 * */ @@ -125,8 +128,7 @@ public Object apply(Object input) { private Object route(Object input, boolean originalInputIsPublisher) { FunctionInvocationWrapper function = null; - if (input instanceof Message) { - Message message = (Message) input; + if (input instanceof Message message) { if (this.routingCallback != null) { String functionDefinition = this.routingCallback.routingResult(message); if (StringUtils.hasText(functionDefinition)) { @@ -153,7 +155,7 @@ else if (StringUtils.hasText(functionProperties.getDefinition())) { } } } - else if (input instanceof Publisher) { + else if (input instanceof Publisher publisher) { if (StringUtils.hasText(functionProperties.getDefinition())) { function = functionFromDefinition(functionProperties.getDefinition()); } @@ -161,9 +163,9 @@ 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)); + return input instanceof Mono mono + ? Mono.from(mono).map(v -> route(v, originalInputIsPublisher)) + : Flux.from(publisher).map(v -> route(v, originalInputIsPublisher)); } } else { @@ -182,7 +184,7 @@ else if (StringUtils.hasText(functionProperties.getRoutingExpression())) { } } - if (function.getTarget().equals(this)) { + if (this.equals(function.getTarget())) { 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."); @@ -193,11 +195,31 @@ else if (StringUtils.hasText(functionProperties.getRoutingExpression())) { private FunctionInvocationWrapper locateFunctionFromDefinitionOrExpression(Message message) { for (Entry headerEntry : message.getHeaders().entrySet()) { - if (headerEntry.getKey().equalsIgnoreCase(FunctionProperties.FUNCTION_DEFINITION)) { - return functionFromDefinition((String) headerEntry.getValue()); + String headerKey = headerEntry.getKey(); + Object headerValue = headerEntry.getValue(); + + if (headerKey == null || headerValue == null) { + continue; } - else if (headerEntry.getKey().equalsIgnoreCase(FunctionProperties.ROUTING_EXPRESSION)) { - return this.functionFromExpression((String) headerEntry.getValue(), message, true); + + boolean isFunctionDefinition = FunctionProperties.FUNCTION_DEFINITION.equalsIgnoreCase(headerKey); + boolean isRoutingExpression = FunctionProperties.ROUTING_EXPRESSION.equalsIgnoreCase(headerKey); + + if (isFunctionDefinition) { + if (headerValue instanceof String definition) { + return functionFromDefinition(definition); + } + else if (headerValue instanceof List definitions && !definitions.isEmpty()) { + return functionFromDefinition(definitions.stream().map(Object::toString).collect(Collectors.joining(","))); + } + } + else if (isRoutingExpression) { + if (headerValue instanceof String expression) { + return functionFromExpression(expression, message, true); + } + else if (headerValue instanceof List expressions && !expressions.isEmpty()) { + return functionFromExpression(expressions.get(0).toString(), message, true); + } } } return null; 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 12c3bdf0a..65d3deafb 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 @@ -19,8 +19,10 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.function.Supplier; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -31,6 +33,7 @@ import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.AbstractMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter; +import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.SmartMessageConverter; import org.springframework.messaging.support.MessageBuilder; @@ -48,13 +51,22 @@ public class SmartCompositeMessageConverter extends CompositeMessageConverter { private Log logger = LogFactory.getLog(this.getClass()); + private final Supplier> messageConverterHelpersSupplier; + public SmartCompositeMessageConverter(Collection converters) { + this(converters, null); + } + + public SmartCompositeMessageConverter(Collection converters, Supplier> messageConverterHelpersSupplier) { super(converters); + this.messageConverterHelpersSupplier = messageConverterHelpersSupplier; } @Override @Nullable public Object fromMessage(Message message, Class targetClass) { + Collection messageConverterHelpers = this.messageConverterHelpersSupplier != null + ? this.messageConverterHelpersSupplier.get() : Collections.emptyList(); for (MessageConverter converter : getConverters()) { if (!(message.getPayload() instanceof byte[]) && targetClass.isInstance(message.getPayload()) && !(message.getPayload() instanceof Collection)) { return message.getPayload(); @@ -69,14 +81,18 @@ public Object fromMessage(Message message, Class targetClass) { if (logger.isWarnEnabled()) { logger.warn("Failure during type conversion by " + converter + ". Will try the next converter.", e); } + this.failConversionIfNecessary(message, messageConverterHelpers, e); } } + return null; } @SuppressWarnings("unchecked") @Override public Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint) { + Collection messageConverterHelpers = this.messageConverterHelpersSupplier != null + ? this.messageConverterHelpersSupplier.get() : Collections.emptyList(); if (!(message.getPayload() instanceof byte[]) && targetClass.isInstance(message.getPayload()) && !(message.getPayload() instanceof Collection)) { return message.getPayload(); } @@ -105,8 +121,12 @@ public Object fromMessage(Message message, Class targetClass, @Nullable Ob } } } + if (!isConverted) { + this.postProcessBatchMessage(message, messageConverterHelpers, resultList.size()); + this.failConversionIfNecessary(message, messageConverterHelpers, null); + } } - result = resultList; + return resultList; } else { for (MessageConverter converter : getConverters()) { @@ -120,10 +140,25 @@ public Object fromMessage(Message message, Class targetClass, @Nullable Ob } } } - + this.failConversionIfNecessary(message, messageConverterHelpers, null); return result; } + private void failConversionIfNecessary(Message message, Collection messageConverterHelpers, Throwable t) { + for (MessageConverterHelper messageConverterHelper : messageConverterHelpers) { + if (messageConverterHelper.shouldFailIfCantConvert(message, t)) { + throw new MessageConversionException("Failed to convert Message: " + message + + ". None of the available Message converters were able to convert this Message"); + } + } + } + + private void postProcessBatchMessage(Message message, Collection messageConverterHelpers, int index) { + for (MessageConverterHelper messageConverterHelper : messageConverterHelpers) { + messageConverterHelper.postProcessBatchMessageOnFailure(message, index); + } + } + @Override @Nullable public Message toMessage(Object payload, @Nullable MessageHeaders headers) { diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java index fe501485a..2288aca36 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JacksonMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2025 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. @@ -25,6 +25,9 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + /** * @author Dave Syer @@ -32,6 +35,8 @@ */ public class JacksonMapper extends JsonMapper { + private static Log logger = LogFactory.getLog(JacksonMapper.class); + private final ObjectMapper mapper; public JacksonMapper(ObjectMapper mapper) { @@ -75,7 +80,9 @@ public byte[] toJson(Object value) { jsonBytes = this.mapper.writeValueAsBytes(value); } catch (Exception e) { - //ignore and let other converters have a chance + if (logger.isTraceEnabled()) { + logger.trace("Failed to writeValueAsBytes: " + value, e); + } } } return jsonBytes; diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java index 781181fe4..4fb91f775 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/json/JsonMapper.java @@ -24,13 +24,14 @@ import java.util.HashSet; import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; @@ -137,33 +138,33 @@ public static boolean isJsonStringRepresentsCollection(Object value) { && !value.getClass().getPackage().getName().startsWith("reactor.util.function")) { return true; } - if (value instanceof byte[]) { - value = new String((byte[]) value, StandardCharsets.UTF_8); + if (value instanceof byte[] byteValue) { + value = new String(byteValue, StandardCharsets.UTF_8); } - if (value instanceof String) { + if (value instanceof String stringValue) { try { - new JSONArray((String) value); + JsonNode node = mapper.readTree(stringValue); + return node instanceof ArrayNode; } - catch (JSONException e) { + catch (JsonProcessingException e) { return false; } - return true; } return false; } public static boolean isJsonStringRepresentsMap(Object value) { - if (value instanceof byte[]) { - value = new String((byte[]) value, StandardCharsets.UTF_8); + if (value instanceof byte[] byteValue) { + value = new String(byteValue, StandardCharsets.UTF_8); } - if (value instanceof String) { + if (value instanceof String stringValue) { try { - new JSONObject(value); + JsonNode node = mapper.readTree(stringValue); + return node instanceof ObjectNode; } - catch (JSONException e) { + catch (JsonProcessingException e) { return false; } - return true; } return false; } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/FunctionMessageUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/FunctionMessageUtils.java index 76a6974c0..8e021650c 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/FunctionMessageUtils.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/FunctionMessageUtils.java @@ -17,6 +17,8 @@ package org.springframework.cloud.function.utils; +import java.util.Locale; + import org.springframework.cloud.function.context.message.MessageUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -59,7 +61,7 @@ else if (key.startsWith("aws_")) { else if (key.startsWith("solace_")) { return "solace"; } - else if (key.toLowerCase().equals("user-agent") || key.toLowerCase().equals("accept-encoding") || key.toLowerCase().equals("host")) { + else if (key.toLowerCase(Locale.ROOT).equals("user-agent") || key.toLowerCase(Locale.ROOT).equals("accept-encoding") || key.toLowerCase(Locale.ROOT).equals("host")) { return "http"; } // add rsocket diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/JsonMasker.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/JsonMasker.java new file mode 100644 index 000000000..325b5a2b3 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/utils/JsonMasker.java @@ -0,0 +1,160 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.utils; + +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.locks.ReentrantLock; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cloud.function.json.JacksonMapper; +import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.util.ClassUtils; + + +/** + * @author Oleg Zhurakousky + * @author Omer Celik + */ +public final class JsonMasker { + + private static final Log logger = LogFactory.getLog(JsonMasker.class); + + private static JsonMasker jsonMasker; + + private final JacksonMapper mapper; + + private final Set keysToMask; + + private static final ReentrantLock globalLock = new ReentrantLock(); + + private JsonMasker() { + this.keysToMask = loadKeys(); + this.mapper = new JacksonMapper(new ObjectMapper()); + + } + + /** + * Double-Checked Locking Optimization was used to avoid unnecessary locking overhead. + */ + public static JsonMasker INSTANCE() { + if (jsonMasker == null) { + try { + globalLock.lock(); + if (jsonMasker == null) { + jsonMasker = new JsonMasker(); + } + } + finally { + globalLock.unlock(); + } + } + return jsonMasker; + } + + public static JsonMasker INSTANCE(Set keysToMask) { + try { + globalLock.lock(); + INSTANCE().addKeys(keysToMask); + return jsonMasker; + } + finally { + globalLock.unlock(); + } + } + + public String[] getKeysToMask() { + return keysToMask.toArray(new String[0]); + } + + public String mask(Object json) { + if (!JsonMapper.isJsonString(json)) { + return (String) json; + } + Object map = this.mapper.fromJson(json, Object.class); + return this.iterate(map); + } + + @SuppressWarnings({ "unchecked" }) + private String iterate(Object json) { + if (json instanceof Collection arrayValue) { + for (Object element : arrayValue) { + if (element instanceof Map mapElement) { + for (Map.Entry entry : ((Map) mapElement).entrySet()) { + this.doMask(entry.getKey(), entry); + } + } + } + } + else if (json instanceof Map mapElement) { + for (Map.Entry entry : ((Map) mapElement).entrySet()) { + this.doMask(entry.getKey(), entry); + } + } + return new String(this.mapper.toJson(json), StandardCharsets.UTF_8); + } + + private void doMask(String key, Map.Entry entry) { + if (this.keysToMask.contains(key)) { + entry.setValue("*******"); + } + else if (entry.getValue() instanceof Map) { + this.iterate(entry.getValue()); + } + else if (entry.getValue() instanceof Collection) { + this.iterate(entry.getValue()); + } + } + + private static Set loadKeys() { + Set finalKeysToMask = new TreeSet<>(); + try { + Enumeration resources = ClassUtils.getDefaultClassLoader().getResources("META-INF/mask.keys"); + while (resources.hasMoreElements()) { + URI uri = resources.nextElement().toURI(); + List lines = Files.readAllLines(Path.of(uri)); + for (String line : lines) { + // need to split in case if delimited + String[] keys = line.split(","); + for (int i = 0; i < keys.length; i++) { + finalKeysToMask.add(keys[i].trim()); + } + } + } + } + catch (Exception e) { + logger.warn("Failed to load keys to mask. No keys will be masked", e); + } + return finalKeysToMask; + } + + private void addKeys(Set keys) { + this.keysToMask.addAll(keys); + } +} diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/actuator/FunctionsEndpointTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/actuator/FunctionsEndpointTests.java new file mode 100644 index 000000000..f9e9b4794 --- /dev/null +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/actuator/FunctionsEndpointTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021-2025 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.actuator; + +import java.util.Locale; +import java.util.Map; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Oleg Zhurakousky + */ + +public class FunctionsEndpointTests { + + @Test + public void ensureIneligibleFunctionWontCauseNPE() { + ApplicationContext context = new SpringApplicationBuilder(SampleConfiguration.class) + .run("--spring.cloud.function.ineligible-definitions=echo,uppercase", + "--spring.main.lazy-initialization=true"); + FunctionCatalog catalog = context.getBean(FunctionCatalog.class); + FunctionsEndpoint endpoint = new FunctionsEndpoint(catalog); + Map> allFunctionsinCatalog = endpoint.listAll(); + // implicit assertion - no NPE + assertThat(allFunctionsinCatalog.size()).isEqualTo(2); + assertThat(allFunctionsinCatalog.containsKey("functionRouter")); + assertThat(allFunctionsinCatalog.containsKey("reverse")); + } + + @EnableAutoConfiguration + @Configuration + public static class SampleConfiguration { + + @Bean + public Function echo() { + return v -> v; + } + + @Bean + public Function uppercase() { + return v -> v.toUpperCase(Locale.ROOT); + } + + @Bean + public Function reverse() { + return v -> new StringBuilder(v).reverse().toString(); + } + } +} 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 fd8b3d843..6257e7721 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 @@ -16,6 +16,7 @@ package org.springframework.cloud.function.context; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -81,7 +82,7 @@ public void testNoDoubleRegistrationInHybridModeFluxedFunction() { assertThat((Function) catalog.lookup(Function.class, "hybridFunctionalRegistrationTests.UppercaseFluxFunction")).isNotNull(); } - @SpringBootConfiguration + @SpringBootConfiguration(proxyBeanMethods = false) @ImportAutoConfiguration({ ContextFunctionCatalogAutoConfiguration.class, JacksonAutoConfiguration.class } @@ -90,11 +91,11 @@ public static class UppercaseFunction implements Function { @Override public String apply(String t) { - return t.toUpperCase(); + return t.toUpperCase(Locale.ROOT); } } - @SpringBootConfiguration + @SpringBootConfiguration(proxyBeanMethods = false) @ImportAutoConfiguration({ ContextFunctionCatalogAutoConfiguration.class, JacksonAutoConfiguration.class } @@ -105,11 +106,11 @@ public static class UppercaseMessageFunction implements Function public String apply(Message message) { assertThat(message.getHeaders().get("foo")).isEqualTo("foo"); assertThat(message.getHeaders().get("blah")).isEqualTo("blah"); - return message.getPayload().toUpperCase(); + return message.getPayload().toUpperCase(Locale.ROOT); } } - @SpringBootConfiguration + @SpringBootConfiguration(proxyBeanMethods = false) @ImportAutoConfiguration({ ContextFunctionCatalogAutoConfiguration.class, JacksonAutoConfiguration.class } @@ -118,7 +119,7 @@ public static class UppercaseFluxFunction implements Function, Flux @Override public Flux apply(Flux flux) { - return flux.map(v -> v.toUpperCase()); + return flux.map(v -> v.toUpperCase(Locale.ROOT)); } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryMultiInOutTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryMultiInOutTests.java index 4039e1cd2..2c979741a 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryMultiInOutTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistryMultiInOutTests.java @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.function.BiFunction; import java.util.function.Function; @@ -303,7 +304,7 @@ protected static class SampleFunctionConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } // ============= MULTI-INPUT and MULTI-OUTPUT functions ============ 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 e39d71f29..e20a1ccd5 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 @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-2025 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. @@ -27,8 +27,10 @@ import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -41,7 +43,7 @@ import java.util.stream.Collectors; import com.fasterxml.jackson.databind.JsonNode; -import org.junit.jupiter.api.Assertions; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -52,12 +54,12 @@ import reactor.util.function.Tuple3; import reactor.util.function.Tuples; +import org.springframework.beans.factory.FactoryBean; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.json.JsonMapper; import org.springframework.context.ApplicationContext; @@ -78,11 +80,11 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; /** * * @author Oleg Zhurakousky + * @author Artem Bilan * */ public class BeanFactoryAwareFunctionRegistryTests { @@ -115,6 +117,75 @@ public void testEmptyPojoConversion() { assertThat(result).isEqualTo("{}"); } + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testCompositionWithNonExistingFunction() throws Exception { + FunctionCatalog catalog = this.configureCatalog(CompositionWithNullReturnInBetween.class); + for (int i = 0; i < 10; i++) { + catalog.lookup("echo1|any"); + } + Field functionRegistrationsField = ReflectionUtils.findField(catalog.getClass(), "functionRegistrations"); + functionRegistrationsField.setAccessible(true); + Set functionRegistrations = (Set) functionRegistrationsField.get(catalog); + assertThat(functionRegistrations.size()).isEqualTo(1); + FunctionRegistration registration = functionRegistrations.iterator().next(); + assertThat(registration.getNames().size()).isEqualTo(1); + assertThat(registration.getNames().iterator().next()).isEqualTo("echo1"); + + for (int i = 0; i < 10; i++) { + catalog.lookup("echo1|any|foo|bar|bye"); + } + assertThat(functionRegistrations.size()).isEqualTo(1); + registration = functionRegistrations.iterator().next(); + assertThat(registration.getNames().size()).isEqualTo(1); + assertThat(registration.getNames().iterator().next()).isEqualTo("echo1"); + } + + @Test + public void testCompositionReactiveSupplierWithImplicitConsumer() throws Exception { + FunctionCatalog catalog = this.configureCatalog(CompositionReactiveSupplierWithConsumer.class); + FunctionInvocationWrapper function = catalog.lookup("supplyPrimitive|consume"); + function.apply(null); + assertThat(CompositionReactiveSupplierWithConsumer.results.size()).isEqualTo(2); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(0)).isEqualTo(1); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(1)).isEqualTo(2); + CompositionReactiveSupplierWithConsumer.results.clear(); + + function = catalog.lookup("supplyMessage|consume"); + function.apply(null); + assertThat(CompositionReactiveSupplierWithConsumer.results.size()).isEqualTo(2); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(0)).isEqualTo(1); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(1)).isEqualTo(2); + CompositionReactiveSupplierWithConsumer.results.clear(); + + function = catalog.lookup("functionMessage|consume"); + function.apply(Flux.fromArray(new Message[] {MessageBuilder.withPayload("ricky").build(), MessageBuilder.withPayload("bubbles").build()})); + assertThat(CompositionReactiveSupplierWithConsumer.results.size()).isEqualTo(2); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(0)).isEqualTo("RICKY"); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(1)).isEqualTo("BUBBLES"); + CompositionReactiveSupplierWithConsumer.results.clear(); + + function = catalog.lookup("functionPrimitive|consume"); + function.apply(Flux.fromArray(new String[] {"ricky", "bubbles"})); + assertThat(CompositionReactiveSupplierWithConsumer.results.size()).isEqualTo(2); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(0)).isEqualTo("RICKY"); + assertThat(CompositionReactiveSupplierWithConsumer.results.get(1)).isEqualTo("BUBBLES"); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Test + public void testMessageWithArrayAsPayload() throws Exception { + FunctionCatalog catalog = this.configureCatalog(MessageWithArrayAsPayload.class); + FunctionInvocationWrapper function = catalog.lookup("myFunction"); + + List payload = List.of("Ricky", "Julien", "Bubbles"); + + Message result = (Message) function.apply(MessageBuilder.withPayload(payload).build()); + + assertThat(((Collection) result.getPayload())).isNotEmpty(); + + } + @SuppressWarnings({ "rawtypes", "unchecked" }) @Test public void testCompositionWithNullReturnInBetween() { @@ -141,7 +212,7 @@ public void testFunctionEligibilityFiltering() { } } System.out.println(registeredFunction); - assertThat(registeredFunction.size()).isEqualTo(2); + //assertThat(registeredFunction.size()).isEqualTo(5); assertThat((FunctionInvocationWrapper) catalog.lookup("asJsonNode")).isNull(); } @@ -300,7 +371,7 @@ public void testReactiveVoidInputFunctionAsSupplier() { @Test - public void testComposition() { + public void testComposition() throws Exception { FunctionCatalog catalog = this.configureCatalog(); Function, Flux> fluxFunction = catalog.lookup("uppercase|reverseFlux"); @@ -323,8 +394,24 @@ public void testComposition() { assertThat(result.get(0)).isEqualTo("OLLEH"); assertThat(result.get(1)).isEqualTo("EYB"); - Function function = catalog.lookup("uppercase|reverse"); + FunctionInvocationWrapper function = catalog.lookup("uppercase|reverse"); assertThat(function.apply("foo")).isEqualTo("OOF"); + + Object target = function.getTarget(); + Field arg1Field = ReflectionUtils.findField(target.getClass(), "arg$1"); + arg1Field.setAccessible(true); + FunctionInvocationWrapper functionUppercase = (FunctionInvocationWrapper) arg1Field.get(target); + + Field arg2Field = ReflectionUtils.findField(target.getClass(), "arg$2"); + arg2Field.setAccessible(true); + FunctionInvocationWrapper functionReverse = (FunctionInvocationWrapper) arg2Field.get(target); + + assertThat(functionUppercase.isSkipInputConversion()).isFalse(); + assertThat(functionReverse.isSkipInputConversion()).isFalse(); + + function.setSkipInputConversion(true); + assertThat(functionUppercase.isSkipInputConversion()).isTrue(); + assertThat(functionReverse.isSkipInputConversion()).isTrue(); } @Test @@ -346,9 +433,13 @@ public void testCompositionSupplierAndFunction() { public void testReactiveFunctionWithImperativeInputAndOutputFail() { FunctionCatalog catalog = this.configureCatalog(); Function reverse = catalog.lookup("reverseFlux"); - Assertions.assertThrows(ClassCastException.class, () -> { + try { String result = reverse.apply("reverseFlux"); - }); + Assertions.fail(); + } + catch (ClassCastException e) { + // ignore + } } @Test @@ -584,46 +675,6 @@ public void testWithComplexHierarchyAndTypeConversion() { assertThat(f.apply(Flux.just(25)).blockFirst()).isEqualTo(25); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Test - public void testRegisteringWithTypeThatDoesNotMatchDiscoveredType() { - FunctionCatalog catalog = this.configureCatalog(EmptyConfiguration.class); - Function func = catalog.lookup("func"); - assertThat(func).isNull(); - FunctionRegistry registry = (FunctionRegistry) catalog; - try { - FunctionRegistration registration = new FunctionRegistration(new MyFunction(), "a") - .type(FunctionTypeUtils.functionType(Integer.class, String.class)); - registry.register(registration); - fail(); - } - catch (IllegalArgumentException e) { - // good as we expect it to fail - } - // - try { - FunctionRegistration registration = new FunctionRegistration(new MyFunction(), "b") - .type(FunctionTypeUtils.functionType(String.class, Integer.class)); - registry.register(registration); - fail(); - } - catch (IllegalArgumentException e) { - // good as we expect it to fail - } - // - FunctionRegistration c = new FunctionRegistration(new MyFunction(), "c") - .type(FunctionTypeUtils.functionType(String.class, String.class)); - registry.register(c); - // - FunctionRegistration d = new FunctionRegistration(new RawFunction(), "d") - .type(FunctionTypeUtils.functionType(Person.class, String.class)); - registry.register(d); - // - FunctionRegistration e = new FunctionRegistration(new RawFunction(), "e") - .type(FunctionTypeUtils.functionType(Object.class, Object.class)); - registry.register(e); - } - @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void testNoConversionOnInputMapIfInputIsMap() { @@ -804,6 +855,14 @@ public void testArrayPayloadOnFluxFunction() throws Exception { assertThat(result.size()).isEqualTo(3); } + @Test + void functionFromFactoryBeanIsProperlyResolved() { + FunctionCatalog catalog = configureCatalog(); + Function numberToStringFactoryBean = catalog.lookup("numberToStringFactoryBean"); + assertThat(numberToStringFactoryBean).isNotNull(); + assertThat(numberToStringFactoryBean.apply(1)).isEqualTo("1"); + } + @Test // see GH-707 public void testConcurrencyOnLookup() throws Exception { @@ -830,7 +889,7 @@ public static class PojoToMessageFunctionCompositionConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean @@ -845,12 +904,12 @@ public Function toJson() { @Bean public Function, String> uppercasePerson() { - return v -> v.getPayload().getName().toUpperCase(); + return v -> v.getPayload().getName().toUpperCase(Locale.ROOT); } } @EnableAutoConfiguration - @Configuration + @Configuration(proxyBeanMethods = false) public static class JsonNodeConfiguration { @Bean public Function, String> messageAsJsonNode() { @@ -1035,7 +1094,7 @@ protected boolean supports(Class clazz) { protected static class WrappedWithAroundAdviseConfiguration { @Bean public Function, Message> uppercase() { - return v -> MessageBuilder.withPayload(v.getPayload().toUpperCase()).copyHeaders(v.getHeaders()).build(); + return v -> MessageBuilder.withPayload(v.getPayload().toUpperCase(Locale.ROOT)).copyHeaders(v.getHeaders()).build(); } @Bean @@ -1062,7 +1121,7 @@ protected static class InputHeaderPropagationConfiguration { @Bean public Function uppercase() { - return x -> x.toUpperCase(); + return x -> x.toUpperCase(Locale.ROOT); } } @@ -1080,7 +1139,7 @@ public Function>>, Flux> echoGenericO @Bean public Function uppercasePerson() { return person -> { - return new Person(person.getName().toUpperCase(), person.getId()); + return new Person(person.getName().toUpperCase(Locale.ROOT), person.getId()); }; } @@ -1092,7 +1151,7 @@ public Supplier numberword() { @Bean public BiFunction biFuncUpperCase() { return (p, h) -> { - return p.toUpperCase(); + return p.toUpperCase(Locale.ROOT); }; } @@ -1106,7 +1165,7 @@ public Function, Person> maptopojo() { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean @@ -1128,7 +1187,7 @@ public Function consumerFunction() { @Bean public Function, Flux> uppercaseFlux() { - return flux -> flux.map(v -> v.toUpperCase()); + return flux -> flux.map(v -> v.toUpperCase(Locale.ROOT)); } @Bean @@ -1255,6 +1314,23 @@ public Consumer> reactiveConsumer() { public Consumer> reactivePojoConsumer() { return flux -> flux.subscribe(v -> consumerInputRef.set(v)); } + + @Bean + FactoryBean> numberToStringFactoryBean() { + return new FactoryBean<>() { + + @Override + public Function getObject() { + return Number::toString; + } + + @Override + public Class getObjectType() { + return Function.class; + } + + }; + } } @EnableAutoConfiguration @@ -1350,7 +1426,7 @@ public String toString() { } @EnableAutoConfiguration - @Configuration + @Configuration(proxyBeanMethods = false) @Component public static class MyFunction implements Function { @@ -1397,7 +1473,7 @@ public Integer apply(String t) { } @EnableAutoConfiguration - @Configuration + @Configuration(proxyBeanMethods = false) @Component public static class MultipleOrderedAcceptValuesAsMessageOutputConfiguration implements Function> { @@ -1413,7 +1489,7 @@ public Message apply(String t) { public static class ComplexTypeFunctionConfiguration { @Bean public Function, String> function() { - return v -> v.getData().getName().toUpperCase(); + return v -> v.getData().getName().toUpperCase(Locale.ROOT); } } @@ -1484,4 +1560,56 @@ public Function echo2() { } } + @EnableAutoConfiguration + @Configuration // s-c-f-1141 + @SuppressWarnings({"unchecked", "rawtypes"}) + public static class CompositionReactiveSupplierWithConsumer { + private static List results = new ArrayList<>(); + + @Bean + public Function, Flux> functionPrimitive() { + return flux -> flux.map(v -> v.toUpperCase(Locale.ROOT)); + } + + @Bean + public Function>, Flux>> functionMessage() { + return flux -> flux.map(v -> MessageBuilder.withPayload(v.getPayload().toUpperCase(Locale.ROOT)).build()); + } + + @Bean + public Supplier>> supplyMessage() { + return () -> { + return Flux.fromArray( + new Message[] { MessageBuilder.withPayload(1).build(), MessageBuilder.withPayload(2).build() }); + }; + } + + @Bean + public Supplier> supplyPrimitive() { + return () -> { + return Flux.fromArray( + new Integer[] { 1, 2}); + }; + } + + @Bean + public Consumer consume() { + return v -> { + if (v instanceof Message vMessage) { + v = vMessage.getPayload(); + } + results.add(v); + }; + } + } + + @EnableAutoConfiguration + @Configuration + public static class MessageWithArrayAsPayload { + + @Bean + public Function, Message> myFunction() { + return msg -> msg; + } + } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwarePojoFunctionRegistryTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwarePojoFunctionRegistryTests.java index accc4c5ba..89986397e 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwarePojoFunctionRegistryTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwarePojoFunctionRegistryTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.context.catalog; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -101,7 +102,7 @@ public void testWithPojoFunctionComposition() { @EnableAutoConfiguration - @Configuration + @Configuration(proxyBeanMethods = false) protected static class SampleFunctionConfiguration { @Bean @@ -123,7 +124,7 @@ public Function func() { // POJO Function that implements Function private static class MyFunction implements Function { public String uppercase(String value) { - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } @Override @@ -135,7 +136,7 @@ public String apply(String t) { // POJO Function public static class MyFunctionLike { public String uppercase(String value) { - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } } 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 ee1dfb29b..9d7ecf6b5 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 @@ -20,13 +20,23 @@ 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.DoubleConsumer; +import java.util.function.DoubleFunction; +import java.util.function.DoubleSupplier; import java.util.function.Function; +import java.util.function.IntConsumer; +import java.util.function.IntFunction; +import java.util.function.IntSupplier; +import java.util.function.LongConsumer; +import java.util.function.LongFunction; +import java.util.function.LongSupplier; import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -34,10 +44,8 @@ import reactor.util.function.Tuple2; import reactor.util.function.Tuple3; -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; @@ -47,14 +55,21 @@ * */ @SuppressWarnings("unused") -public class FunctionTypeUtilsTests { +public class FunctionTypeUtilsTests { + + @Test + public void testDiscoverFunctionalMethod() throws Exception { + Method method = FunctionTypeUtils.discoverFunctionalMethod(SampleEventConsumer.class); + assertThat(method.getName()).isEqualTo("accept"); + } @Test public void testFunctionTypeFrom() throws Exception { Type type = FunctionTypeUtils.discoverFunctionTypeFromClass(SimpleConsumer.class); - assertThat(type).isInstanceOf(ParameterizedType.class); - Type wrapperType = ((ParameterizedType) type).getActualTypeArguments()[0]; - assertThat(wrapperType).isInstanceOf(ParameterizedType.class); + //assertThat(type).isInstanceOf(ParameterizedType.class); + Type wrapperType = FunctionTypeUtils.getInputType(type); +// Type wrapperType = ((ParameterizedType) type).getActualTypeArguments()[0]; +// assertThat(wrapperType).isInstanceOf(ParameterizedType.class); assertThat(wrapperType.getTypeName()).contains("Flux"); Type innerWrapperType = ((ParameterizedType) wrapperType).getActualTypeArguments()[0]; @@ -63,7 +78,6 @@ public void testFunctionTypeFrom() throws Exception { Type targetType = ((ParameterizedType) innerWrapperType).getActualTypeArguments()[0]; assertThat(targetType).isEqualTo(String.class); - System.out.println(); } @Test @@ -102,21 +116,101 @@ public void testIsTypeCollection() { assertThat(FunctionTypeUtils.isTypeCollection(new ParameterizedTypeReference>>>() { }.getType())).isFalse(); } - @Test - public void testNoNpeFromIsMessage() { - FunctionTypeUtilsTests testService = new FunctionTypeUtilsTests<>(); +// @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(); +// } + + //@Test + public void testPrimitiveFunctionInputTypes() { + Type type = FunctionTypeUtils.discoverFunctionTypeFromClass(IntConsumer.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(IntConsumer.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(IntSupplier.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(IntSupplier.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(IntFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(IntFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(ToIntFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(ToIntFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(LongConsumer.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(LongConsumer.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(LongSupplier.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(LongSupplier.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(LongFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(LongFunction.class); - Method methodUnderTest = - ReflectionUtils.findMethod(testService.getClass(), "notAMessageMethod", AtomicReference.class); - MethodParameter methodParameter = MethodParameter.forExecutable(methodUnderTest, 0); + type = FunctionTypeUtils.discoverFunctionTypeFromClass(ToLongFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(ToLongFunction.class); - assertThat(FunctionTypeUtils.isMessage(methodParameter.getGenericParameterType())).isFalse(); + type = FunctionTypeUtils.discoverFunctionTypeFromClass(DoubleConsumer.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(DoubleConsumer.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(DoubleSupplier.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(DoubleSupplier.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(DoubleFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(DoubleFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(ToDoubleFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getInputType(type))).isAssignableFrom(ToDoubleFunction.class); } - void notAMessageMethod(AtomicReference payload) { + //@Test + public void testPrimitiveFunctionOutputTypes() { + Type type = FunctionTypeUtils.discoverFunctionTypeFromClass(IntConsumer.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(IntConsumer.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(IntSupplier.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(IntSupplier.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(IntFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(IntFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(ToIntFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(ToIntFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(LongConsumer.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(LongConsumer.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(LongSupplier.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(LongSupplier.class); + + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(LongFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(LongFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(ToLongFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(ToLongFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(DoubleConsumer.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(DoubleConsumer.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(DoubleSupplier.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(DoubleSupplier.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(DoubleFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(DoubleFunction.class); + + type = FunctionTypeUtils.discoverFunctionTypeFromClass(ToDoubleFunction.class); + assertThat(FunctionTypeUtils.getRawType(FunctionTypeUtils.getOutputType(type))).isAssignableFrom(ToDoubleFunction.class); } +// void notAMessageMethod(AtomicReference payload) { +// +// } + private static Function function() { return null; } @@ -205,4 +299,29 @@ public Flux apply(Flux inFlux) { return inFlux.map(v -> Integer.parseInt(v)); } } + + public static abstract class AbstractConsumer implements Consumer> { + + @Override + public final void accept(Message message) { + if (message == null) { + return; + } + + doAccept(message.getPayload()); + } + + protected abstract void doAccept(C payload); + } + + public static class SampleEventConsumer extends AbstractConsumer { + @Override + protected void doAccept(SampleData data) { + } + } + + public static class SampleData { + + } + } 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 5f2fb2fb9..5645582c4 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 @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.UUID; @@ -40,7 +41,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.gson.Gson; import com.google.protobuf.StringValue; -import org.junit.jupiter.api.Assertions; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -75,6 +76,7 @@ import org.springframework.messaging.converter.ByteArrayMessageConverter; import org.springframework.messaging.converter.CompositeMessageConverter; import org.springframework.messaging.converter.MessageConverter; +import org.springframework.messaging.converter.ProtobufMessageConverter; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.support.MessageBuilder; import org.springframework.util.MimeType; @@ -115,7 +117,7 @@ public void testSCF1094(String stringValue) throws IOException { Function getValue = msg -> msg != null ? msg.getValue() : null; Type functionType = ResolvableType.forClassWithGenerics(Function.class, ResolvableType.forClass(StringValue.class), ResolvableType.forClass(String.class)).getType(); - var catalog = new SimpleFunctionRegistry(this.conversionService, this.messageConverter, new JacksonMapper(new ObjectMapper())); + var catalog = new SimpleFunctionRegistry(this.conversionService, new CompositeMessageConverter(List.of(new ProtobufMessageConverter())), new JacksonMapper(new ObjectMapper())); catalog.register(new FunctionRegistration<>(getValue, "getValue").type(functionType)); FunctionInvocationWrapper lookedUpFunction = catalog.lookup("getValue"); @@ -129,22 +131,7 @@ public void testSCF1094(String stringValue) throws IOException { .setHeader("contentType", "application/x-protobuf") .build(); - if (stringValue.equals("aaaaaaaaaa")) { - try { - lookedUpFunction.apply(inputMessage); - } - catch (Exception ex) { - assertThat(ex).isInstanceOf(ClassCastException.class); - } - } - else { - try { - lookedUpFunction.apply(inputMessage); - } - catch (Exception ex) { - assertThat(ex).isInstanceOf(IllegalStateException.class); - } - } + assertThat(lookedUpFunction.apply(inputMessage)).isEqualTo(stringValue); } @SuppressWarnings("rawtypes") @@ -504,7 +491,8 @@ public void testReactiveFunctionMessages() { .build() )); - Assertions.assertIterableEquals(result.blockFirst(), Arrays.asList("item1", "item2")); + List blockFirst = result.blockFirst(); + Assertions.assertThatIterable(blockFirst).isEqualTo(Arrays.asList("item1", "item2")); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -665,7 +653,7 @@ void biConsumerUserFunctionTypeIsKnownInFunctionInvocationWrapper() { } public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @@ -779,7 +767,7 @@ private static class UpperCase implements Function { @Override public String apply(String t) { - return t.toUpperCase(); + return t.toUpperCase(Locale.ROOT); } } @@ -798,7 +786,7 @@ private static class UpperCaseMessage @Override public Message apply(Message t) { - return MessageBuilder.withPayload(t.getPayload().toUpperCase()) + return MessageBuilder.withPayload(t.getPayload().toUpperCase(Locale.ROOT)) .copyHeaders(t.getHeaders()).build(); } 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 index 62b8d06fe..0b19ba8cf 100644 --- 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 @@ -17,6 +17,7 @@ package org.springframework.cloud.function.context.config; import io.cloudevents.spring.messaging.CloudEventMessageConverter; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -35,6 +36,7 @@ * * @author Chris Bono */ +@Disabled public class ContextFunctionCatalogAutoConfigurationConditionalLoadingTests { protected final ApplicationContextRunner contextRunner = new ApplicationContextRunner() diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java index 6e4230b0d..be388ab95 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -40,9 +41,6 @@ import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; -import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.function.context.FunctionCatalog; @@ -60,7 +58,6 @@ import org.springframework.context.annotation.Import; import org.springframework.core.env.Environment; import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.DescriptiveResource; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; @@ -430,16 +427,16 @@ public void registrationBean() { .isInstanceOf(Function.class); } - @Test - public void factoryBeanFunction() { - create(FactoryBeanConfiguration.class); - assertThat(this.context.getBean("function")).isInstanceOf(Function.class); - assertThat((Function) this.catalog.lookup(Function.class, "function")) - .isInstanceOf(Function.class); - Function, Flux> f = this.catalog.lookup(Function.class, - "function"); - assertThat(f.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO-bar"); - } +// @Test +// public void factoryBeanFunction() { +// create(FactoryBeanConfiguration.class); +// assertThat(this.context.getBean("function")).isInstanceOf(Function.class); +// assertThat((Function) this.catalog.lookup(Function.class, "function")) +// .isInstanceOf(Function.class); +// Function, Flux> f = this.catalog.lookup(Function.class, +// "function"); +// assertThat(f.apply(Flux.just("foo")).blockFirst()).isEqualTo("FOO-bar"); +// } @Test public void functionCatalogDependentBeanFactoryPostProcessor() { @@ -476,7 +473,7 @@ protected static class SimpleConfiguration { @Bean public Function function() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } @Bean @@ -543,7 +540,7 @@ protected static class DependencyInjectionConfiguration { @Bean public Function foos(String foo) { - return value -> new Foo(foo + ": " + value.toUpperCase()); + return value -> new Foo(foo + ": " + value.toUpperCase(Locale.ROOT)); } @Bean @@ -554,13 +551,13 @@ public String value() { } @EnableAutoConfiguration - @Configuration("foos") + @Configuration(proxyBeanMethods = false, value = "foos") protected static class FunctionConfiguration implements Function, Flux> { @Override public Flux apply(Flux flux) { - return flux.map(foo -> new Foo(value() + ": " + foo.toUpperCase())); + return flux.map(foo -> new Foo(value() + ": " + foo.toUpperCase(Locale.ROOT))); } @Bean @@ -588,7 +585,7 @@ protected static class AmbiguousConfiguration { @Bean public Function foos() { - return value -> new Foo(value.toUpperCase()); + return value -> new Foo(value.toUpperCase(Locale.ROOT)); } @Bean @@ -605,7 +602,7 @@ protected static class MultipleConfiguration { @Bean public Function foos() { - return value -> new Foo(value.toUpperCase()); + return value -> new Foo(value.toUpperCase(Locale.ROOT)); } @Bean @@ -632,7 +629,7 @@ protected static class GenericConfiguration { @Bean public Function, Map> function() { return m -> m.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), - e -> e.getValue().toString().toUpperCase())); + e -> e.getValue().toString().toUpperCase(Locale.ROOT))); } } @@ -707,7 +704,7 @@ protected static class ComponentScanBeanConfiguration { } @EnableAutoConfiguration - @Configuration + @Configuration(proxyBeanMethods = false) @ComponentScan(basePackageClasses = ScannedFunction.class) protected static class ComponentScanConfiguration { @@ -726,7 +723,7 @@ protected static class GenericFluxConfiguration { @Bean public Function>, Flux>> function() { return flux -> flux.map(m -> m.entrySet().stream().collect(Collectors - .toMap(e -> e.getKey(), e -> e.getValue().toString().toUpperCase()))); + .toMap(e -> e.getKey(), e -> e.getValue().toString().toUpperCase(Locale.ROOT)))); } } @@ -738,7 +735,7 @@ protected static class FluxMessageConfiguration { @Bean public Function>, Flux>> function() { return flux -> flux.map(m -> MessageBuilder - .withPayload(m.getPayload().toUpperCase()).build()); + .withPayload(m.getPayload().toUpperCase(Locale.ROOT)).build()); } } @@ -750,7 +747,7 @@ protected static class PublisherMessageConfiguration { @Bean public Function>, Publisher>> function() { return flux -> Flux.from(flux).map(m -> MessageBuilder - .withPayload(m.getPayload().toUpperCase()).build()); + .withPayload(m.getPayload().toUpperCase(Locale.ROOT)).build()); } } @@ -784,7 +781,7 @@ protected static class MessageConfiguration { @Bean public Function, Message> function() { - return m -> MessageBuilder.withPayload(m.getPayload().toUpperCase()).build(); + return m -> MessageBuilder.withPayload(m.getPayload().toUpperCase(Locale.ROOT)).build(); } } @@ -796,7 +793,7 @@ protected static class QualifiedConfiguration { @Bean @Qualifier("other") public Function function() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } @@ -807,7 +804,7 @@ protected static class AliasConfiguration { @Bean({ "function", "other" }) public Function function() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } @@ -824,32 +821,32 @@ public FunctionRegistration> registration() { @Bean public Function function() { - return value -> value.toUpperCase(); - } - - } - - @EnableAutoConfiguration - @Configuration - protected static class FactoryBeanConfiguration - implements BeanDefinitionRegistryPostProcessor { - - @Override - public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) - throws BeansException { - RootBeanDefinition beanDefinition = new RootBeanDefinition( - FunctionFactoryBean.class); - beanDefinition.setSource(new DescriptiveResource("Function")); - registry.registerBeanDefinition("function", beanDefinition); - } - - @Override - public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) - throws BeansException { - - } - - } + return value -> value.toUpperCase(Locale.ROOT); + } + + } + +// @EnableAutoConfiguration +// @Configuration(proxyBeanMethods = false ) +// protected static class FactoryBeanConfiguration +// implements BeanDefinitionRegistryPostProcessor { +// +// @Override +// public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) +// throws BeansException { +// RootBeanDefinition beanDefinition = new RootBeanDefinition( +// FunctionFactoryBean.class); +// beanDefinition.setSource(new DescriptiveResource("Function")); +// registry.registerBeanDefinition("function", beanDefinition); +// } +// +// @Override +// public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) +// throws BeansException { +// +// } +// +// } private static class FunctionFactoryBean extends AbstractFactoryBean> { @@ -861,7 +858,7 @@ public Class getObjectType() { @Override protected Function createInstance() throws Exception { - return s -> s.toUpperCase() + "-bar"; + return s -> s.toUpperCase(Locale.ROOT) + "-bar"; } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializerTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializerTests.java index 3b3075a73..fd00ccacb 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializerTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializerTests.java @@ -20,14 +20,15 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import com.google.gson.Gson; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -115,14 +116,13 @@ public void compose() { @Test public void missingType() { - Assertions.assertThrows(BeanCreationException.class, () -> { + try { create(MissingTypeConfiguration.class); - assertThat(this.context.getBean("function")) - .isInstanceOf(FunctionRegistration.class); - assertThat((Function) this.catalog.lookup(Function.class, "function")) - .isInstanceOf(Function.class); - // TODO: support for type inference from functional bean registrations - }); + Assertions.fail(); + } + catch (BeanCreationException e) { + // ignore, the test call must fail + } } @Test @@ -235,7 +235,7 @@ public void initialize(GenericApplicationContext context) { @Bean public Function function() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } @@ -264,7 +264,7 @@ public void initialize(GenericApplicationContext context) { public Function function() { return person -> { Person p = new Person(); - p.setName(person.getName().toUpperCase()); + p.setName(person.getName().toUpperCase(Locale.ROOT)); return p; }; } @@ -359,7 +359,7 @@ public void initialize(GenericApplicationContext context) { @Bean public Function foos(String foo) { - return value -> new Foo(foo + ": " + value.toUpperCase()); + return value -> new Foo(foo + ": " + value.toUpperCase(Locale.ROOT)); } @Bean @@ -383,7 +383,7 @@ public void initialize(GenericApplicationContext context) { @Override public Flux apply(Flux flux) { - return flux.map(foo -> new Foo(value() + ": " + foo.toUpperCase())); + return flux.map(foo -> new Foo(value() + ": " + foo.toUpperCase(Locale.ROOT))); } @Bean 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 20a3d6b02..366dd5475 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 @@ -17,11 +17,12 @@ package org.springframework.cloud.function.context.config; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; @@ -42,7 +43,6 @@ import org.springframework.messaging.support.MessageBuilder; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; /** * @@ -82,7 +82,7 @@ public void testDefaultRouting() { assertThat(function).isNotNull(); try { function.apply(message); - fail(); + Assertions.fail(); } catch (Exception e) { // Good @@ -98,7 +98,7 @@ public void testDefaultRouting() { @SuppressWarnings({ "unchecked", "rawtypes" }) @Test - public void testInvocationWithMessageAndHeader() { + public void testInvocationWithMessageAndStringHeader() { FunctionCatalog functionCatalog = this.configureCatalog(); Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); assertThat(function).isNotNull(); @@ -107,6 +107,57 @@ public void testInvocationWithMessageAndHeader() { assertThat(function.apply(message)).isEqualTo("olleh"); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testInvocationWithMessageAndListOfSingleElementHeader() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".definition", List.of("reverse")) + .build(); + assertThat(function.apply(message)).isEqualTo("olleh"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testCompositionWithMessageAndListOfMultipleElementsHeader() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".definition", + List.of("reverse", "uppercase")) + .build(); + assertThat(function.apply(message)).isEqualTo("OLLEH"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testInvocationWithMessageAndListOfSingleRoutingExpression() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".routing-expression", + List.of("'reverse'")) + .build(); + assertThat(function.apply(message)).isEqualTo("olleh"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testInvocationWithMessageAndListOfMultipleRoutingExpressions() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".routing-expression", + List.of("'uppercase'", "'reverse'")) + .build(); + assertThat(function.apply(message)).isEqualTo("HELLO"); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void testRoutingSimpleInputWithReactiveFunctionWithMessageHeader() { @@ -155,7 +206,7 @@ public void failWithHeaderProvidedExpressionAccessingRuntime() { .build(); try { function.apply(message); - fail(); + Assertions.fail(); } catch (Exception e) { assertThat(e.getMessage()).isEqualTo("EL1005E: Type cannot be found 'java.lang.Runtime'"); diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/scan/TestFunction.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/scan/TestFunction.java index ad12ee4b9..5a8c2cb7e 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/scan/TestFunction.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/scan/TestFunction.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.context.scan; +import java.util.Locale; import java.util.function.Function; /** @@ -26,7 +27,7 @@ public class TestFunction implements Function { @Override public String apply(String t) { - return t.toUpperCase(); + return t.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/string/FunctionalStringSourceTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/string/FunctionalStringSourceTests.java index 585e2043a..f336e476c 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/string/FunctionalStringSourceTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/string/FunctionalStringSourceTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.context.string; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -52,7 +53,7 @@ protected static class TestConfiguration implements Function { @Override public String apply(String value) { - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/test/FunctionalTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/test/FunctionalTests.java index a22958732..b7cd6bf65 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/test/FunctionalTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/test/FunctionalTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.context.test; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -49,7 +50,7 @@ protected static class TestConfiguration implements Function { @Override public String apply(String value) { - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/inject/FooConfiguration.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/inject/FooConfiguration.java index e7db39122..a62f653b3 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/inject/FooConfiguration.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/inject/FooConfiguration.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.inject; +import java.util.Locale; import java.util.function.Function; import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfigurationTests.Foo; @@ -27,7 +28,7 @@ public class FooConfiguration { @Bean public Function foos(String foo) { - return value -> new Foo(foo + ": " + value.toUpperCase()); + return value -> new Foo(foo + ": " + value.toUpperCase(Locale.ROOT)); } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/scan/ScannedFunction.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/scan/ScannedFunction.java index 83f49d38d..a829ad573 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/scan/ScannedFunction.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/scan/ScannedFunction.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.scan; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -33,7 +34,7 @@ public class ScannedFunction @Override public Map apply(Map m) { return m.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), - e -> e.getValue().toString().toUpperCase())); + e -> e.getValue().toString().toUpperCase(Locale.ROOT))); } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/test/GenericFunction.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/test/GenericFunction.java index 46ae2b9d4..4389f4aff 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/test/GenericFunction.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/test/GenericFunction.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.test; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -33,7 +34,7 @@ public class GenericFunction { @Bean public Function, Map> function() { return m -> m.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), - e -> e.getValue().toString().toUpperCase())); + e -> e.getValue().toString().toUpperCase(Locale.ROOT))); } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/utils/JsonMapperTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/utils/JsonMapperTests.java index 30207d367..1b7e28eef 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/utils/JsonMapperTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/utils/JsonMapperTests.java @@ -16,6 +16,10 @@ package org.springframework.cloud.function.utils; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.util.Date; import java.util.List; import java.util.stream.Stream; @@ -27,10 +31,15 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.cloud.function.json.GsonMapper; import org.springframework.cloud.function.json.JacksonMapper; import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; import org.springframework.core.ResolvableType; +import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -67,6 +76,26 @@ public void objectNode_isJsonStringRepresentsCollection() { assertThat(JsonMapper.isJsonStringRepresentsCollection(nodeAsString)).isFalse(); } + // see https://github.com/spring-cloud/spring-cloud-function/issues/1189 + @Test + public void testJsonDateTimeConversion() { + ApplicationContext context = SpringApplication.run(EmptyConfiguration.class); + JsonMapper jsonMapper = context.getBean(JsonMapper.class); + StringVsTimestamp dom = new StringVsTimestamp("2024-10-16T16:13:29.964361+02:00"); + String convertedJson = new String(jsonMapper.toJson(dom), StandardCharsets.UTF_8); + assertThat(convertedJson).contains("\"zonedDateTime\":\"2024-10-16T16:13:29.964361+02:00\""); + } + + @Test + public void testKotlinModuleRegistration() throws Exception { + ApplicationContext context = SpringApplication.run(EmptyConfiguration.class); + JsonMapper jsonMapper = context.getBean(JsonMapper.class); + Field mapperField = ReflectionUtils.findField(jsonMapper.getClass(), "mapper"); + mapperField.setAccessible(true); + ObjectMapper mapper = (ObjectMapper) mapperField.get(jsonMapper); + assertThat(mapper.getRegisteredModuleIds()).contains("com.fasterxml.jackson.module.kotlin.KotlinModule"); + } + @ParameterizedTest @MethodSource("params") public void vanillaArray(JsonMapper mapper) { @@ -140,4 +169,48 @@ public void setValue(String value) { } + @EnableAutoConfiguration + @Configuration + static class EmptyConfiguration { + + } + + static class StringVsTimestamp { + + private String type; + + private Date date; + + private ZonedDateTime zonedDateTime; + + StringVsTimestamp(String zonedDate) { + type = "StringVsTimestamp"; + date = new Date(); + zonedDateTime = ZonedDateTime.parse(zonedDate); + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public ZonedDateTime getZonedDateTime() { + return zonedDateTime; + } + + public void setZonedDateTime(ZonedDateTime zonedDateTime) { + this.zonedDateTime = zonedDateTime; + } + } } diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/utils/JsonMaskerTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/utils/JsonMaskerTests.java new file mode 100644 index 000000000..37c7ce0e8 --- /dev/null +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/utils/JsonMaskerTests.java @@ -0,0 +1,279 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.utils; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import org.assertj.core.util.Arrays; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.function.json.JacksonMapper; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class JsonMaskerTests { + + private String event = "{\n" + + " \"Records\": [\n" + + " {\n" + + " \"eventID\": \"f07f8ca4b0b26cb9c4e5e77e69f274ee\",\n" + + " \"eventName\": \"INSERT\",\n" + + " \"eventVersion\": \"1.1\",\n" + + " \"eventSource\": \"aws:dynamodb\",\n" + + " \"awsRegion\": \"us-east-1\",\n" + + " \"userIdentity\":{\n" + + " \"type\":\"Service\",\n" + + " \"principalId\":\"dynamodb.amazonaws.com\"\n" + + " },\n" + + " \"dynamodb\": {\n" + + " \"ApproximateCreationDateTime\": 1.684934517E9,\n" + + " \"Keys\": {\n" + + " \"val\": {\n" + + " \"S\": \"data\"\n" + + " },\n" + + " \"key\": {\n" + + " \"S\": \"binary\"\n" + + " }\n" + + " },\n" + + " \"NewImage\": {\n" + + " \"val\": {\n" + + " \"S\": \"data\"\n" + + " },\n" + + " \"asdf1\": {\n" + + " \"B\": \"AAEqQQ==\"\n" + + " },\n" + + " \"asdf2\": {\n" + + " \"BS\": [\n" + + " \"AAEqQQ==\",\n" + + " \"QSoBAA==\"\n" + + " ]\n" + + " },\n" + + " \"key\": {\n" + + " \"S\": \"binary\"\n" + + " }\n" + + " },\n" + + " \"SequenceNumber\": \"1405400000000002063282832\",\n" + + " \"SizeBytes\": 54,\n" + + " \"StreamViewType\": \"NEW_AND_OLD_IMAGES\"\n" + + " },\n" + + " \"eventSourceARN\": \"arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000\"\n" + + " },\n" + + " {\n" + + " \"eventID\": \"f07f8ca4b0b26cb9c4e5e77e42f274ee\",\n" + + " \"eventName\": \"INSERT\",\n" + + " \"eventVersion\": \"1.1\",\n" + + " \"eventSource\": \"aws:dynamodb\",\n" + + " \"awsRegion\": \"us-east-1\",\n" + + " \"dynamodb\": {\n" + + " \"ApproximateCreationDateTime\": 1480642020,\n" + + " \"Keys\": {\n" + + " \"val\": {\n" + + " \"S\": \"data\"\n" + + " },\n" + + " \"key\": {\n" + + " \"S\": \"binary\"\n" + + " }\n" + + " },\n" + + " \"NewImage\": {\n" + + " \"val\": {\n" + + " \"S\": \"data\"\n" + + " },\n" + + " \"asdf1\": {\n" + + " \"B\": \"AAEqQQ==\"\n" + + " },\n" + + " \"b2\": {\n" + + " \"B\": \"test\"\n" + + " },\n" + + " \"asdf2\": {\n" + + " \"BS\": [\n" + + " \"AAEqQQ==\",\n" + + " \"QSoBAA==\",\n" + + " \"AAEqQQ==\"\n" + + " ]\n" + + " },\n" + + " \"key\": {\n" + + " \"S\": \"binary\"\n" + + " },\n" + + " \"Binary\": {\n" + + " \"B\": \"AAEqQQ==\"\n" + + " },\n" + + " \"Boolean\": {\n" + + " \"BOOL\": true\n" + + " },\n" + + " \"BinarySet\": {\n" + + " \"BS\": [\n" + + " \"AAEqQQ==\",\n" + + " \"AAEqQQ==\"\n" + + " ]\n" + + " },\n" + + " \"List\": {\n" + + " \"L\": [\n" + + " {\n" + + " \"S\": \"Cookies\"\n" + + " },\n" + + " {\n" + + " \"S\": \"Coffee\"\n" + + " },\n" + + " {\n" + + " \"N\": \"3.14159\"\n" + + " }\n" + + " ]\n" + + " },\n" + + " \"Map\": {\n" + + " \"M\": {\n" + + " \"Name\": {\n" + + " \"S\": \"Joe\"\n" + + " },\n" + + " \"Age\": {\n" + + " \"N\": \"35\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"FloatNumber\": {\n" + + " \"N\": \"123.45\"\n" + + " },\n" + + " \"IntegerNumber\": {\n" + + " \"N\": \"123\"\n" + + " },\n" + + " \"NumberSet\": {\n" + + " \"NS\": [\n" + + " \"1234\",\n" + + " \"567.8\"\n" + + " ]\n" + + " },\n" + + " \"Null\": {\n" + + " \"NULL\": true\n" + + " },\n" + + " \"String\": {\n" + + " \"S\": \"Hello\"\n" + + " },\n" + + " \"StringSet\": {\n" + + " \"SS\": [\n" + + " \"Giraffe\",\n" + + " \"Zebra\"\n" + + " ]\n" + + " },\n" + + " \"EmptyStringSet\": {\n" + + " \"SS\": []\n" + + " }\n" + + " },\n" + + " \"SequenceNumber\": \"1405400000000002063282832\",\n" + + " \"SizeBytes\": 54,\n" + + " \"StreamViewType\": \"NEW_AND_OLD_IMAGES\"\n" + + " },\n" + + " \"eventSourceARN\": \"arn:aws:dynamodb:us-east-1:123456789012:table/Example-Table/stream/2016-12-01T00:00:00.000\"\n" + + " }\n" + + " ]\n" + + "}"; + + private List maskedKeys = new ArrayList<>(); + + @Test + public void validateMasking() throws Exception { + JacksonMapper mapper = new JacksonMapper(new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)); + Map map = mapper.fromJson(event, Map.class); + + JsonMasker masker = JsonMasker.INSTANCE(); + String[] keysToMask = masker.getKeysToMask(); + assertThat(keysToMask).contains("eventSourceARN", "asdf1", "SS"); + + String maskedJson = masker.mask(event); + System.out.println(maskedJson); + map = mapper.fromJson(maskedJson, Map.class); + + this.iterate(map, Arrays.asList(keysToMask)); + assertThat(maskedKeys.size()).isEqualTo(6); + assertThat(maskedKeys.get(0)).isEqualTo("asdf1"); + assertThat(maskedKeys.get(1)).isEqualTo("eventSourceARN"); + assertThat(maskedKeys.get(2)).isEqualTo("asdf1"); + assertThat(maskedKeys.get(3)).isEqualTo("SS"); + assertThat(maskedKeys.get(4)).isEqualTo("SS"); + assertThat(maskedKeys.get(5)).isEqualTo("eventSourceARN"); + + Field jsonMaskerField = ReflectionUtils.findField(JsonMasker.class, "jsonMasker"); + jsonMaskerField.setAccessible(true); + jsonMaskerField.set(masker, null); + } + + @Test + public void validateMaskingWithAdditionalKeys() throws Exception { + JacksonMapper mapper = new JacksonMapper(new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT)); + Map map = mapper.fromJson(event, Map.class); + + JsonMasker masker = JsonMasker.INSTANCE(Set.of("foo", "bar")); + String[] keysToMask = masker.getKeysToMask(); + assertThat(keysToMask).contains("eventSourceARN", "asdf1", "SS", "foo", "bar"); + + String maskedJson = masker.mask(event); + System.out.println(maskedJson); + map = mapper.fromJson(maskedJson, Map.class); + + this.iterate(map, Arrays.asList(keysToMask)); + assertThat(maskedKeys.size()).isEqualTo(6); + assertThat(maskedKeys.get(0)).isEqualTo("asdf1"); + assertThat(maskedKeys.get(1)).isEqualTo("eventSourceARN"); + assertThat(maskedKeys.get(2)).isEqualTo("asdf1"); + assertThat(maskedKeys.get(3)).isEqualTo("SS"); + assertThat(maskedKeys.get(4)).isEqualTo("SS"); + assertThat(maskedKeys.get(5)).isEqualTo("eventSourceARN"); + + Field jsonMaskerField = ReflectionUtils.findField(JsonMasker.class, "jsonMasker"); + jsonMaskerField.setAccessible(true); + jsonMaskerField.set(masker, null); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void iterate(Object json, List keysToMask) { + if (json instanceof Collection arrayValue) { + for (Object element : arrayValue) { + if (element instanceof Map mapElement) { + for (Map.Entry entry : ((Map) mapElement).entrySet()) { + this.doMask(entry.getKey(), entry, keysToMask); + } + } + } + } + else if (json instanceof Map mapElement) { + for (Map.Entry entry : ((Map) mapElement).entrySet()) { + this.doMask(entry.getKey(), entry, keysToMask); + } + } + } + + @SuppressWarnings("rawtypes") + private void doMask(String key, Map.Entry entry, List keysToMask) { + if (keysToMask.contains(key)) { + System.out.println("Masked: " + entry.getKey()); + maskedKeys.add(key); + } + else if (entry.getValue() instanceof Map) { + this.iterate(entry.getValue(), keysToMask); + } + else if (entry.getValue() instanceof Collection) { + this.iterate(entry.getValue(), keysToMask); + } + } +} diff --git a/spring-cloud-function-context/src/test/resources/META-INF/mask.keys b/spring-cloud-function-context/src/test/resources/META-INF/mask.keys new file mode 100644 index 000000000..fadb6a069 --- /dev/null +++ b/spring-cloud-function-context/src/test/resources/META-INF/mask.keys @@ -0,0 +1,2 @@ +eventSourceARN +asdf1, SS diff --git a/spring-cloud-function-core/pom.xml b/spring-cloud-function-core/pom.xml index 6ed0f8a95..abffa328e 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 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT diff --git a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/core/FunctionInvocationHelper.java b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/core/FunctionInvocationHelper.java index ccd11c6e2..0698fdbff 100644 --- a/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/core/FunctionInvocationHelper.java +++ b/spring-cloud-function-core/src/main/java/org/springframework/cloud/function/core/FunctionInvocationHelper.java @@ -21,14 +21,15 @@ /** * * @author Oleg Zhurakousky + * @author John Blum * @since 3.1 * */ public interface FunctionInvocationHelper { - default boolean isRetainOuputAsMessage(I input) { + default boolean isRetainOutputAsMessage(I input) { return true; - }; + } default I preProcessInput(I input, Object inputConverter) { return input; diff --git a/spring-cloud-function-dependencies/pom.xml b/spring-cloud-function-dependencies/pom.xml index d50451a2c..ff07852c2 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 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT spring-cloud-function-dependencies - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT pom Spring Cloud Function Dependencies Spring Cloud Function Dependencies @@ -58,22 +58,22 @@ org.springframework.cloud - spring-cloud-function-adapter-gcp + spring-cloud-function-adapter-azure-web ${project.version} org.springframework.cloud - spring-cloud-function-integration + spring-cloud-function-adapter-gcp ${project.version} org.springframework.cloud - spring-cloud-function-kotlin + spring-cloud-function-integration ${project.version} org.springframework.cloud - spring-cloud-function-rsocket + spring-cloud-function-kotlin ${project.version} diff --git a/spring-cloud-function-deployer/pom.xml b/spring-cloud-function-deployer/pom.xml index 8ebaf4ba7..8627af699 100644 --- a/spring-cloud-function-deployer/pom.xml +++ b/spring-cloud-function-deployer/pom.xml @@ -10,11 +10,15 @@ org.springframework.cloud spring-cloud-function-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT 17 + 2.16.1 + 3.9.6 + 1.9.18 + 3.5.3 @@ -34,11 +38,7 @@ org.springframework.cloud spring-cloud-function-context - - org.springframework.cloud - spring-cloud-deployer-resource-maven - 2.5.1 - + org.springframework.boot spring-boot-starter-test @@ -55,6 +55,66 @@ 2.2.0 test + + + org.apache.maven + maven-model-builder + ${maven.version} + + + + javax.inject + javax.inject + + + + + org.apache.maven + maven-resolver-provider + ${maven.version} + + + + javax.inject + javax.inject + + + + + org.apache.maven.resolver + maven-resolver-connector-basic + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-file + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-http + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-transport-wagon + ${maven-resolver.version} + + + org.apache.maven.resolver + maven-resolver-impl + ${maven-resolver.version} + + + commons-io + commons-io + 2.19.0 + + + org.apache.maven.wagon + wagon-http + 3.5.3 + 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 957a6c8ee..c0f5468eb 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-multi/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootapp-multi/pom.xml @@ -12,14 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 17 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.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 e18606230..f4a9cea98 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,14 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 17 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/SimpleFunctionAppApplication.java b/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/SimpleFunctionAppApplication.java index ed8f694ef..54f837216 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/SimpleFunctionAppApplication.java +++ b/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/SimpleFunctionAppApplication.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -25,7 +26,7 @@ public Function uppercasePerson() { return person -> { Person p = new Person(); p.setId(person.getId()); - p.setName(person.getName().toUpperCase()); + p.setName(person.getName().toUpperCase(Locale.ROOT)); return p; }; } diff --git a/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/UpperCaseFunction.java b/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/UpperCaseFunction.java index 735ad65fb..723035247 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/UpperCaseFunction.java +++ b/spring-cloud-function-deployer/src/it/bootapp-with-javax/src/main/java/function/example/UpperCaseFunction.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; import javax.mail.Address; @@ -17,7 +18,7 @@ public String apply(String value) { catch (AddressException e) { throw new IllegalStateException("Failed to create and address: ", e); } - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } 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 556ed55e0..b68c2d90f 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,14 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 17 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/SimpleFunctionAppApplication.java b/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/SimpleFunctionAppApplication.java index 63e0719c3..40ee3eaa3 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/SimpleFunctionAppApplication.java +++ b/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/SimpleFunctionAppApplication.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -31,7 +32,7 @@ public Function uppercasePerson() { return person -> { Person p = new Person(); p.setId(person.getId()); - p.setName(person.getName().toUpperCase()); + p.setName(person.getName().toUpperCase(Locale.ROOT)); return p; }; } diff --git a/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/UpperCaseFunction.java b/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/UpperCaseFunction.java index 859a54a58..7c3571826 100644 --- a/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/UpperCaseFunction.java +++ b/spring-cloud-function-deployer/src/it/bootapp-with-scf/src/main/java/function/example/UpperCaseFunction.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; public class UpperCaseFunction implements Function { @@ -7,7 +8,7 @@ public class UpperCaseFunction implements Function { @Override public String apply(String value) { System.out.println("Uppercasing " + value); - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-deployer/src/it/bootapp/pom.xml b/spring-cloud-function-deployer/src/it/bootapp/pom.xml index fc5579636..13c212f1c 100644 --- a/spring-cloud-function-deployer/src/it/bootapp/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootapp/pom.xml @@ -12,14 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 17 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/SimpleFunctionAppApplication.java b/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/SimpleFunctionAppApplication.java index ed8f694ef..54f837216 100644 --- a/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/SimpleFunctionAppApplication.java +++ b/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/SimpleFunctionAppApplication.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -25,7 +26,7 @@ public Function uppercasePerson() { return person -> { Person p = new Person(); p.setId(person.getId()); - p.setName(person.getName().toUpperCase()); + p.setName(person.getName().toUpperCase(Locale.ROOT)); return p; }; } diff --git a/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/UpperCaseFunction.java b/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/UpperCaseFunction.java index 859a54a58..7c3571826 100644 --- a/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/UpperCaseFunction.java +++ b/spring-cloud-function-deployer/src/it/bootapp/src/main/java/function/example/UpperCaseFunction.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; public class UpperCaseFunction implements Function { @@ -7,7 +8,7 @@ public class UpperCaseFunction implements Function { @Override public String apply(String value) { System.out.println("Uppercasing " + value); - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } 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 eced046e7..2339f9341 100644 --- a/spring-cloud-function-deployer/src/it/bootjar-multi/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootjar-multi/pom.xml @@ -12,14 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 17 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootjar/pom.xml b/spring-cloud-function-deployer/src/it/bootjar/pom.xml index 1c842fd23..b5c33b3f9 100644 --- a/spring-cloud-function-deployer/src/it/bootjar/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootjar/pom.xml @@ -12,14 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 17 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootjar/src/main/java/function/example/UpperCaseFunction.java b/spring-cloud-function-deployer/src/it/bootjar/src/main/java/function/example/UpperCaseFunction.java index 859a54a58..7c3571826 100644 --- a/spring-cloud-function-deployer/src/it/bootjar/src/main/java/function/example/UpperCaseFunction.java +++ b/spring-cloud-function-deployer/src/it/bootjar/src/main/java/function/example/UpperCaseFunction.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; public class UpperCaseFunction implements Function { @@ -7,7 +8,7 @@ public class UpperCaseFunction implements Function { @Override public String apply(String value) { System.out.println("Uppercasing " + value); - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml b/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml index 8eab07b64..f963c216b 100644 --- a/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml +++ b/spring-cloud-function-deployer/src/it/bootjarnostart/pom.xml @@ -12,14 +12,14 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 17 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-deployer/src/it/bootjarnostart/src/main/java/function/example/UpperCaseFunction.java b/spring-cloud-function-deployer/src/it/bootjarnostart/src/main/java/function/example/UpperCaseFunction.java index 859a54a58..7c3571826 100644 --- a/spring-cloud-function-deployer/src/it/bootjarnostart/src/main/java/function/example/UpperCaseFunction.java +++ b/spring-cloud-function-deployer/src/it/bootjarnostart/src/main/java/function/example/UpperCaseFunction.java @@ -1,5 +1,6 @@ package function.example; +import java.util.Locale; import java.util.function.Function; public class UpperCaseFunction implements Function { @@ -7,7 +8,7 @@ public class UpperCaseFunction implements Function { @Override public String apply(String value) { System.out.println("Uppercasing " + value); - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-deployer/src/it/simplestjar/pom.xml b/spring-cloud-function-deployer/src/it/simplestjar/pom.xml index 8eaa026d9..0afe9a1cb 100644 --- a/spring-cloud-function-deployer/src/it/simplestjar/pom.xml +++ b/spring-cloud-function-deployer/src/it/simplestjar/pom.xml @@ -28,7 +28,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 package diff --git a/spring-cloud-function-deployer/src/it/simplestjarcs/src/main/java/functions/UpperCaseFunction.java b/spring-cloud-function-deployer/src/it/simplestjarcs/src/main/java/functions/UpperCaseFunction.java index 07f535dc1..1cca83da1 100644 --- a/spring-cloud-function-deployer/src/it/simplestjarcs/src/main/java/functions/UpperCaseFunction.java +++ b/spring-cloud-function-deployer/src/it/simplestjarcs/src/main/java/functions/UpperCaseFunction.java @@ -1,5 +1,6 @@ package functions; +import java.util.Locale; import java.util.function.Function; public class UpperCaseFunction implements Function { @@ -7,7 +8,7 @@ public class UpperCaseFunction implements Function { @Override public String apply(String value) { System.out.println("Uppercasing " + value); - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java index 2105c5b0d..607b5e60f 100644 --- a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/FunctionDeployerConfiguration.java @@ -34,10 +34,10 @@ import org.springframework.boot.loader.archive.Archive; import org.springframework.boot.loader.archive.ExplodedArchive; import org.springframework.boot.loader.archive.JarFileArchive; -import org.springframework.cloud.deployer.resource.maven.MavenProperties; -import org.springframework.cloud.deployer.resource.maven.MavenResourceLoader; import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionRegistry; +import org.springframework.cloud.function.deployer.utils.MavenProperties; +import org.springframework.cloud.function.deployer.utils.MavenResourceLoader; import org.springframework.context.ApplicationContext; import org.springframework.context.SmartLifecycle; import org.springframework.context.annotation.Bean; diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/LoggingRepositoryListener.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/LoggingRepositoryListener.java new file mode 100644 index 000000000..8a6d2ce2d --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/LoggingRepositoryListener.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019-2025 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.deployer.utils; + +import org.eclipse.aether.AbstractRepositoryListener; +import org.eclipse.aether.RepositoryEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Corneil du Plessis + */ +public class LoggingRepositoryListener extends AbstractRepositoryListener { + + private static final Logger logger = LoggerFactory.getLogger(LoggingRepositoryListener.class); + + public void artifactDeployed(RepositoryEvent event) { + println("artifactDeployed", event.getArtifact() + " to " + event.getRepository()); + } + + public void artifactDeploying(RepositoryEvent event) { + println("artifactDeploying", event.getArtifact() + " to " + event.getRepository()); + } + + public void artifactDescriptorInvalid(RepositoryEvent event) { + println("artifactDescriptorInvalid", "for " + event.getArtifact() + ": " + event.getException().getMessage()); + } + + public void artifactDescriptorMissing(RepositoryEvent event) { + println("artifactDescriptorMissing", "for " + event.getArtifact()); + } + + public void artifactInstalled(RepositoryEvent event) { + println("artifactInstalled", event.getArtifact() + " to " + event.getFile()); + } + + public void artifactInstalling(RepositoryEvent event) { + println("artifactInstalling", event.getArtifact() + " to " + event.getFile()); + } + + public void artifactResolved(RepositoryEvent event) { + println("artifactResolved", event.getArtifact() + " from " + event.getRepository()); + } + + public void artifactDownloading(RepositoryEvent event) { + println("artifactDownloading", event.getArtifact() + " from " + event.getRepository()); + } + + public void artifactDownloaded(RepositoryEvent event) { + println("artifactDownloaded", event.getArtifact() + " from " + event.getRepository()); + } + + public void artifactResolving(RepositoryEvent event) { + println("artifactResolving", event.getArtifact().toString()); + } + + public void metadataDeployed(RepositoryEvent event) { + println("metadataDeployed", event.getMetadata() + " to " + event.getRepository()); + } + + public void metadataDeploying(RepositoryEvent event) { + println("metadataDeploying", event.getMetadata() + " to " + event.getRepository()); + } + + public void metadataInstalled(RepositoryEvent event) { + println("metadataInstalled", event.getMetadata() + " to " + event.getFile()); + } + + public void metadataInstalling(RepositoryEvent event) { + println("metadataInstalling", event.getMetadata() + " to " + event.getFile()); + } + + public void metadataInvalid(RepositoryEvent event) { + println("metadataInvalid", event.getMetadata().toString()); + } + + public void metadataResolved(RepositoryEvent event) { + println("metadataResolved", event.getMetadata() + " from " + event.getRepository()); + } + + public void metadataResolving(RepositoryEvent event) { + println("metadataResolving", event.getMetadata() + " from " + event.getRepository()); + } + + private void println(String event, String message) { + logger.info("Aether Repository - " + event + ": " + message); + } +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenArtifactResolver.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenArtifactResolver.java new file mode 100644 index 000000000..fe3de5a8d --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenArtifactResolver.java @@ -0,0 +1,431 @@ +/* + * Copyright 2019-2025 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.deployer.utils; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.apache.maven.repository.internal.MavenRepositorySystemUtils; +import org.eclipse.aether.ConfigurationProperties; +import org.eclipse.aether.DefaultRepositorySystemSession; +import org.eclipse.aether.RepositorySystem; +import org.eclipse.aether.RepositorySystemSession; +import org.eclipse.aether.artifact.Artifact; +import org.eclipse.aether.artifact.DefaultArtifact; +import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory; +import org.eclipse.aether.impl.DefaultServiceLocator; +import org.eclipse.aether.repository.Authentication; +import org.eclipse.aether.repository.AuthenticationContext; +import org.eclipse.aether.repository.AuthenticationDigest; +import org.eclipse.aether.repository.LocalRepository; +import org.eclipse.aether.repository.Proxy; +import org.eclipse.aether.repository.RemoteRepository; +import org.eclipse.aether.repository.RepositoryPolicy; +import org.eclipse.aether.resolution.ArtifactRequest; +import org.eclipse.aether.resolution.ArtifactResolutionException; +import org.eclipse.aether.resolution.ArtifactResult; +import org.eclipse.aether.resolution.VersionRangeRequest; +import org.eclipse.aether.resolution.VersionRangeResolutionException; +import org.eclipse.aether.resolution.VersionRangeResult; +import org.eclipse.aether.spi.connector.RepositoryConnectorFactory; +import org.eclipse.aether.spi.connector.transport.TransporterFactory; +import org.eclipse.aether.transport.file.FileTransporterFactory; +import org.eclipse.aether.transport.http.HttpTransporterFactory; +import org.eclipse.aether.util.artifact.JavaScopes; +import org.eclipse.aether.util.repository.DefaultProxySelector; +import org.eclipse.aether.version.Version; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Resolves a {@link MavenResource} to + * locate the artifact (uber jar) in a local Maven repository, downloading the latest update from a + * remote repository if necessary. + *

A set of default remote repos (Maven Central, Spring Snapshots, Spring Milestones) will be automatically added to + * the head of the list of remote repos. If the default repo is already explicitly configured (exact match on the repo url) + * then that particular default will be omitted. To skip the automatic default repos behavior altogether, set the + * {@link MavenProperties#isIncludeDefaultRemoteRepos()} property to {@code false}. + * + * @author David Turanski + * @author Mark Fisher + * @author Marius Bogoevici + * @author Ilayaperumal Gopinathan + * @author Donovan Muller + * @author Corneil du Plessis + * @author Chris Bono + */ +class MavenArtifactResolver { + + private static final Logger logger = LoggerFactory.getLogger(MavenArtifactResolver.class); + + private static final String DEFAULT_CONTENT_TYPE = "default"; + + private final RepositorySystem repositorySystem; + + private final MavenProperties properties; + + private final List remoteRepositories = new LinkedList<>(); + + private final Authentication proxyAuthentication; + + /** + * Create an instance using the provided properties. + * + * @param properties the properties for the maven repositories, proxies, and authentication + */ + MavenArtifactResolver(MavenProperties properties) { + Assert.notNull(properties, "MavenProperties must not be null"); + Assert.notNull(properties.getLocalRepository(), "Local repository path cannot be null"); + this.properties = properties; + if (logger.isDebugEnabled()) { + logger.debug("Configured local repository: " + properties.getLocalRepository()); + logger.debug("Configured remote repositories: " + configuredRemoteRepositoriesDescription()); + } + if (isProxyEnabled() && proxyHasCredentials()) { + final String username = this.properties.getProxy().getAuth().getUsername(); + final String password = this.properties.getProxy().getAuth().getPassword(); + this.proxyAuthentication = newAuthentication(username, password); + } + else { + this.proxyAuthentication = null; + } + File localRepository = new File(this.properties.getLocalRepository()); + if (!localRepository.exists()) { + boolean created = localRepository.mkdirs(); + // May have been created by another thread after above check. Double check. + Assert.isTrue(created || localRepository.exists(), + "Unable to create directory for local repository: " + localRepository); + } + + Map defaultRepoUrlsToIds = defaultRemoteRepos(); + + for (Map.Entry entry : this.properties.getRemoteRepositories() + .entrySet()) { + MavenProperties.RemoteRepository remoteRepository = entry.getValue(); + RemoteRepository.Builder remoteRepositoryBuilder = new RemoteRepository.Builder( + entry.getKey(), DEFAULT_CONTENT_TYPE, remoteRepository.getUrl()); + // Update policies when set. + if (remoteRepository.getPolicy() != null) { + remoteRepositoryBuilder.setPolicy(new RepositoryPolicy(remoteRepository.getPolicy().isEnabled(), + remoteRepository.getPolicy().getUpdatePolicy(), + remoteRepository.getPolicy().getChecksumPolicy())); + } + if (remoteRepository.getReleasePolicy() != null) { + remoteRepositoryBuilder + .setReleasePolicy(new RepositoryPolicy(remoteRepository.getReleasePolicy().isEnabled(), + remoteRepository.getReleasePolicy().getUpdatePolicy(), + remoteRepository.getReleasePolicy().getChecksumPolicy())); + } + if (remoteRepository.getSnapshotPolicy() != null) { + remoteRepositoryBuilder + .setSnapshotPolicy(new RepositoryPolicy(remoteRepository.getSnapshotPolicy().isEnabled(), + remoteRepository.getSnapshotPolicy().getUpdatePolicy(), + remoteRepository.getSnapshotPolicy().getChecksumPolicy())); + } + if (remoteRepositoryHasCredentials(remoteRepository)) { + final String username = remoteRepository.getAuth().getUsername(); + final String password = remoteRepository.getAuth().getPassword(); + remoteRepositoryBuilder.setAuthentication(newAuthentication(username, password)); + } + // do not add default repo if explicitly configured + defaultRepoUrlsToIds.remove(remoteRepository.getUrl()); + + RemoteRepository repo = proxyRepoIfProxyEnabled(remoteRepositoryBuilder.build()); + this.remoteRepositories.add(repo); + } + + if (!defaultRepoUrlsToIds.isEmpty() && this.properties.isIncludeDefaultRemoteRepos()) { + List defaultRepos = new ArrayList<>(); + defaultRepoUrlsToIds.forEach((url, id) -> { + if (logger.isDebugEnabled()) { + logger.debug("Adding {} ({}) to remote repositories list", id, url); + } + RemoteRepository defaultRepo = proxyRepoIfProxyEnabled(new RemoteRepository.Builder(id, DEFAULT_CONTENT_TYPE, url).build()); + defaultRepos.add(defaultRepo); + }); + this.remoteRepositories.addAll(0, defaultRepos); + } + if (logger.isDebugEnabled()) { + logger.debug("Using remote repositories: {}", actualRemoteRepositoriesDescription()); + } + this.repositorySystem = newRepositorySystem(); + } + + /** + * Gets the default repos to automatically add. + * @return map of default repos (repo url to repo id) + */ + protected Map defaultRemoteRepos() { + Map defaultRepos = new LinkedHashMap<>(); + defaultRepos.put("https://repo.maven.apache.org/maven2", "mavenCentral-default"); + defaultRepos.put("https://repo.spring.io/snapshot", "springSnapshot-default"); + defaultRepos.put("https://repo.spring.io/milestone", "springMilestone-default"); + return defaultRepos; + } + + private RemoteRepository proxyRepoIfProxyEnabled(RemoteRepository remoteRepo) { + if (!isProxyEnabled()) { + return remoteRepo; + } + Proxy proxy; + MavenProperties.Proxy proxyProperties = this.properties.getProxy(); + if (this.proxyAuthentication != null) { + proxy = new Proxy( + proxyProperties.getProtocol(), + proxyProperties.getHost(), + proxyProperties.getPort(), + this.proxyAuthentication); + } + else { + // if proxy does not require authentication + proxy = new Proxy( + proxyProperties.getProtocol(), + proxyProperties.getHost(), + proxyProperties.getPort()); + } + DefaultProxySelector proxySelector = new DefaultProxySelector(); + proxySelector.add(proxy, this.properties.getProxy().getNonProxyHosts()); + proxy = proxySelector.getProxy(remoteRepo); + + RemoteRepository.Builder remoteRepositoryBuilder = new RemoteRepository.Builder(remoteRepo); + remoteRepositoryBuilder.setProxy(proxy); + return remoteRepositoryBuilder.build(); + } + + /** + * Check if the proxy settings are provided. + * + * @return boolean true if the proxy settings are provided. + */ + private boolean isProxyEnabled() { + return (this.properties.getProxy() != null && + this.properties.getProxy().getHost() != null && + this.properties.getProxy().getPort() > 0); + } + + /** + * Check if the proxy setting has username/password set. + * + * @return boolean true if both the username/password are set + */ + private boolean proxyHasCredentials() { + return (this.properties.getProxy() != null && + this.properties.getProxy().getAuth() != null && + this.properties.getProxy().getAuth().getUsername() != null && + this.properties.getProxy().getAuth().getPassword() != null); + } + + /** + * Check if the {@link MavenProperties.RemoteRepository} setting has username/password set. + * + * @return boolean true if both the username/password are set + */ + private boolean remoteRepositoryHasCredentials(MavenProperties.RemoteRepository remoteRepository) { + return remoteRepository != null && + remoteRepository.getAuth() != null && + remoteRepository.getAuth().getUsername() != null && + remoteRepository.getAuth().getPassword() != null; + } + + /** + * Create an {@link Authentication} given a username/password. + * + * @param username the user + * @param password the password + * @return a configured {@link Authentication} + */ + private Authentication newAuthentication(final String username, final String password) { + return new Authentication() { + + @Override + public void fill(AuthenticationContext context, String key, Map data) { + context.put(AuthenticationContext.USERNAME, username); + context.put(AuthenticationContext.PASSWORD, password); + } + + @Override + public void digest(AuthenticationDigest digest) { + digest.update(AuthenticationContext.USERNAME, username, + AuthenticationContext.PASSWORD, password); + } + }; + } + + DefaultRepositorySystemSession newRepositorySystemSession() { + return this.newRepositorySystemSession(this.repositorySystem, this.properties.getLocalRepository()); + } + + /* + * Create a session to manage remote and local synchronization. + */ + private DefaultRepositorySystemSession newRepositorySystemSession(RepositorySystem system, String localRepoPath) { + DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession(); + LocalRepository localRepo = new LocalRepository(localRepoPath); + session.setLocalRepositoryManager(system.newLocalRepositoryManager(session, localRepo)); + session.setOffline(this.properties.isOffline()); + session.setUpdatePolicy(this.properties.getUpdatePolicy()); + session.setChecksumPolicy(this.properties.getChecksumPolicy()); + if (this.properties.isEnableRepositoryListener()) { + session.setRepositoryListener(new LoggingRepositoryListener()); + } + if (this.properties.getConnectTimeout() != null) { + session.setConfigProperty(ConfigurationProperties.CONNECT_TIMEOUT, this.properties.getConnectTimeout()); + } + if (this.properties.getRequestTimeout() != null) { + session.setConfigProperty(ConfigurationProperties.REQUEST_TIMEOUT, this.properties.getRequestTimeout()); + } + if (isProxyEnabled()) { + DefaultProxySelector proxySelector = new DefaultProxySelector(); + Proxy proxy = new Proxy(this.properties.getProxy().getProtocol(), + this.properties.getProxy().getHost(), + this.properties.getProxy().getPort(), + this.proxyAuthentication); + proxySelector.add(proxy, this.properties.getProxy().getNonProxyHosts()); + session.setProxySelector(proxySelector); + } + // wagon configs + for (Entry entry : this.properties.getRemoteRepositories().entrySet()) { + session.setConfigProperty("aether.connector.wagon.config." + entry.getKey(), entry.getValue().getWagon()); + } + return session; + } + + /* + * Aether's components implement {@link org.eclipse.aether.spi.locator.Service} to ease manual wiring. + * Using the prepopulated {@link DefaultServiceLocator}, we need to register the repository connector + * and transporter factories + */ + private RepositorySystem newRepositorySystem() { + DefaultServiceLocator locator = MavenRepositorySystemUtils.newServiceLocator(); + locator.addService(RepositoryConnectorFactory.class, BasicRepositoryConnectorFactory.class); + locator.addService(TransporterFactory.class, FileTransporterFactory.class); + + locator.addService(TransporterFactory.class, HttpTransporterFactory.class); + + locator.setErrorHandler(new DefaultServiceLocator.ErrorHandler() { + @Override + public void serviceCreationFailed(Class type, Class impl, Throwable exception) { + throw new RuntimeException(exception); + } + }); + return locator.getService(RepositorySystem.class); + } + + /** + * Gets the list of configured remote repositories. + * @return unmodifiable list of configured remote repositories. + */ + List remoteRepositories() { + return Collections.unmodifiableList(this.remoteRepositories); + } + + private String actualRemoteRepositoriesDescription() { + return this.remoteRepositories.stream().map((repo) -> String.format("%s (%s)", repo.getId(), repo.getUrl())) + .collect(Collectors.joining(", ", "[", "]")); + } + + private String configuredRemoteRepositoriesDescription() { + return this.properties.getRemoteRepositories().entrySet().stream() + .map((e) -> String.format("%s (%s)", e.getKey(), e.getValue().getUrl())) + .collect(Collectors.joining(", ", "[", "]")); + } + + List getVersions(String coordinates) { + Artifact artifact = new DefaultArtifact(coordinates); + VersionRangeRequest rangeRequest = new VersionRangeRequest(); + rangeRequest.setArtifact(artifact); + rangeRequest.setRepositories(this.remoteRepositories); + try { + VersionRangeResult versionResult = this.repositorySystem.resolveVersionRange(newRepositorySystemSession(), rangeRequest); + List versions = new ArrayList<>(); + for (Version version: versionResult.getVersions()) { + versions.add(version.toString()); + } + return versions; + } + catch (VersionRangeResolutionException e) { + throw new IllegalStateException(e); + } + } + + /** + * Resolve an artifact and return its location in the local repository. Aether performs the normal + * Maven resolution process ensuring that the latest update is cached to the local repository. + * In addition, if the {@code MavenProperties.resolvePom} flag is true, + * the POM is also resolved and cached. + * @param resource the {@link MavenResource} representing the artifact + * @return a {@link FileSystemResource} representing the resolved artifact in the local repository + * @throws IllegalStateException if the artifact does not exist or the resolution fails + */ + Resource resolve(MavenResource resource) { + Assert.notNull(resource, "MavenResource must not be null"); + validateCoordinates(resource); + RepositorySystemSession session = newRepositorySystemSession(this.repositorySystem, this.properties.getLocalRepository()); + try { + List artifactRequests = new ArrayList<>(2); + if (properties.isResolvePom()) { + artifactRequests.add(new ArtifactRequest(toPomArtifact(resource), this.remoteRepositories, JavaScopes.RUNTIME)); + } + artifactRequests.add(new ArtifactRequest(toJarArtifact(resource), this.remoteRepositories, JavaScopes.RUNTIME)); + List results = this.repositorySystem.resolveArtifacts(session, artifactRequests); + return toResource(results.get(results.size() - 1)); + } + catch (ArtifactResolutionException ex) { + String errorMsg = String.format("Failed to resolve %s using remote repo(s): %s", + resource, actualRemoteRepositoriesDescription()); + throw new IllegalStateException(errorMsg, ex); + } + } + + private void validateCoordinates(MavenResource resource) { + Assert.hasText(resource.getGroupId(), "groupId must not be blank."); + Assert.hasText(resource.getArtifactId(), "artifactId must not be blank."); + Assert.hasText(resource.getExtension(), "extension must not be blank."); + Assert.hasText(resource.getVersion(), "version must not be blank."); + } + + public FileSystemResource toResource(ArtifactResult resolvedArtifact) { + return new FileSystemResource(resolvedArtifact.getArtifact().getFile()); + } + + private Artifact toJarArtifact(MavenResource resource) { + return toArtifact(resource, resource.getExtension()); + } + + private Artifact toPomArtifact(MavenResource resource) { + return toArtifact(resource, "pom"); + } + + private Artifact toArtifact(MavenResource resource, String extension) { + return new DefaultArtifact(resource.getGroupId(), + resource.getArtifactId(), + resource.getClassifier() != null ? resource.getClassifier() : "", + extension, + resource.getVersion()); + } +} diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenProperties.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenProperties.java new file mode 100644 index 000000000..4abc9b464 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenProperties.java @@ -0,0 +1,491 @@ +/* + * Copyright 2019-2025 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.deployer.utils; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; + +/** + * Configuration Properties for Maven. + * + * @author Ilayaperumal Gopinathan + * @author Eric Bottard + * @author Mark Fisher + * @author Donovan Muller + */ +public class MavenProperties { + + /** + * Default file path to a locally available maven repository. + */ + private static String DEFAULT_LOCAL_REPO = System.getProperty("user.home") + + File.separator + ".m2" + File.separator + "repository"; + + /** + * Whether default remote repositories should be automatically included in the list of remote repositories. + */ + private boolean includeDefaultRemoteRepos = true; + + /** + * File path to a locally available maven repository, where artifacts will be downloaded. + */ + private String localRepository = DEFAULT_LOCAL_REPO; + + /** + * Locations of remote maven repositories from which artifacts will be downloaded, if not available locally. + */ + private Map remoteRepositories = new TreeMap<>(); + + /** + * Whether the resolver should operate in offline mode. + */ + private boolean offline; + + /** + * Proxy configuration properties. + */ + private Proxy proxy; + + /** + * The connect timeout. If null, the underlying default will be used. + */ + private Integer connectTimeout; + + /** + * The request timeout. If null, the underlying default will be used. + */ + private Integer requestTimeout; + + /** + * In addition to resolving the JAR artifact, if true, resolve the POM artifact. + * This is consistent with the way that Maven resolves artifacts. + */ + private boolean resolvePom; + + private String updatePolicy; + + private String checksumPolicy; + + /** + * Add the ConsoleRepositoryListener to the session for debugging of artifact resolution. + */ + private boolean enableRepositoryListener = false; + + boolean isIncludeDefaultRemoteRepos() { + return includeDefaultRemoteRepos; + } + + void setIncludeDefaultRemoteRepos(boolean includeDefaultRemoteRepos) { + this.includeDefaultRemoteRepos = includeDefaultRemoteRepos; + } + + /** + * Use maven wagon based transport for http based artifacts. + */ + private boolean useWagon; + + public void setUseWagon(boolean useWagon) { + this.useWagon = useWagon; + } + + public boolean isUseWagon() { + return useWagon; + } + + public boolean isEnableRepositoryListener() { + return enableRepositoryListener; + } + + public void setEnableRepositoryListener(boolean enableRepositoryListener) { + this.enableRepositoryListener = enableRepositoryListener; + } + + public String getUpdatePolicy() { + return updatePolicy; + } + + public void setUpdatePolicy(String updatePolicy) { + this.updatePolicy = updatePolicy; + } + + public String getChecksumPolicy() { + return checksumPolicy; + } + + public void setChecksumPolicy(String checksumPolicy) { + this.checksumPolicy = checksumPolicy; + } + + public Map getRemoteRepositories() { + return remoteRepositories; + } + + public void setRemoteRepositories(final Map remoteRepositories) { + this.remoteRepositories = new TreeMap<>(remoteRepositories); + } + + public void setLocalRepository(String localRepository) { + this.localRepository = localRepository; + } + + public String getLocalRepository() { + return localRepository; + } + + public boolean isOffline() { + return offline; + } + + public void setOffline(Boolean offline) { + this.offline = offline; + } + + public Integer getConnectTimeout() { + return this.connectTimeout; + } + + public void setConnectTimeout(Integer connectTimeout) { + this.connectTimeout = connectTimeout; + } + + public Integer getRequestTimeout() { + return this.requestTimeout; + } + + public void setRequestTimeout(Integer requestTimeout) { + this.requestTimeout = requestTimeout; + } + + public Proxy getProxy() { + return this.proxy; + } + + public void setProxy(Proxy proxy) { + this.proxy = proxy; + } + + public boolean isResolvePom() { + return resolvePom; + } + + public void setResolvePom(final boolean resolvePom) { + this.resolvePom = resolvePom; + } + + public static class Proxy { + + /** + * Protocol to use for proxy settings. + */ + private String protocol = "http"; + + /** + * Host for the proxy. + */ + private String host; + + /** + * Port for the proxy. + */ + private int port; + + /** + * List of non proxy hosts. + */ + private String nonProxyHosts; + + private Authentication auth; + + public String getProtocol() { + return this.protocol; + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getNonProxyHosts() { + return this.nonProxyHosts; + } + + public void setNonProxyHosts(String nonProxyHosts) { + this.nonProxyHosts = nonProxyHosts; + } + + public Authentication getAuth() { + return this.auth; + } + + public void setAuth(Authentication auth) { + this.auth = auth; + } + } + + public enum WagonHttpMethod { + // directly maps to http methods in org.apache.maven.wagon.shared.http.HttpConfiguration + /** + * All methods. + */ + all, + /** + * GET method. + */ + get, + /** + * PUT method. + */ + put, + /** + * HEAD method. + */ + head; + } + + public static class WagonHttpMethodProperties { + // directly maps to settings in org.apache.maven.wagon.shared.http.HttpMethodConfiguration + private boolean usePreemptive; + private boolean useDefaultHeaders; + private Integer connectionTimeout; + private Integer readTimeout; + private Map headers = new HashMap<>(); + private Map params = new HashMap<>(); + + public boolean isUsePreemptive() { + return usePreemptive; + } + + public void setUsePreemptive(boolean usePreemptive) { + this.usePreemptive = usePreemptive; + } + + public boolean isUseDefaultHeaders() { + return useDefaultHeaders; + } + + public void setUseDefaultHeaders(boolean useDefaultHeaders) { + this.useDefaultHeaders = useDefaultHeaders; + } + + public Integer getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(Integer connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public Integer getReadTimeout() { + return readTimeout; + } + + public void setReadTimeout(Integer readTimeout) { + this.readTimeout = readTimeout; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers; + } + + public Map getParams() { + return params; + } + + public void setParams(Map params) { + this.params = params; + } + } + + public static class Wagon { + + private Map http = new HashMap<>(); + + public Map getHttp() { + return http; + } + + public void setHttp(Map http) { + this.http = http; + } + } + + public static class RemoteRepository { + + /** + * URL of the remote maven repository. E.g. https://my.repo.com + */ + private String url; + + private Authentication auth; + + private RepositoryPolicy policy; + + private RepositoryPolicy snapshotPolicy; + + private RepositoryPolicy releasePolicy; + + private Wagon wagon = new Wagon(); + + public RemoteRepository() { + } + + public RemoteRepository(final String url) { + this.url = url; + } + + public RemoteRepository(final String url, final Authentication auth) { + this.url = url; + this.auth = auth; + } + + public Wagon getWagon() { + return wagon; + } + + public void setWagon(Wagon wagon) { + this.wagon = wagon; + } + + public String getUrl() { + return url; + } + + public void setUrl(final String url) { + this.url = url; + } + + public Authentication getAuth() { + return auth; + } + + public void setAuth(final Authentication auth) { + this.auth = auth; + } + + public RepositoryPolicy getPolicy() { + return policy; + } + + public void setPolicy(RepositoryPolicy policy) { + this.policy = policy; + } + + public RepositoryPolicy getSnapshotPolicy() { + return snapshotPolicy; + } + + public void setSnapshotPolicy(RepositoryPolicy snapshotPolicy) { + this.snapshotPolicy = snapshotPolicy; + } + + public RepositoryPolicy getReleasePolicy() { + return releasePolicy; + } + + public void setReleasePolicy(RepositoryPolicy releasePolicy) { + this.releasePolicy = releasePolicy; + } + } + + public static class RepositoryPolicy { + + private boolean enabled = true; + + private String updatePolicy = "always"; + + private String checksumPolicy = "warn"; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getUpdatePolicy() { + return updatePolicy; + } + + public void setUpdatePolicy(String updatePolicy) { + this.updatePolicy = updatePolicy; + } + + public String getChecksumPolicy() { + return checksumPolicy; + } + + public void setChecksumPolicy(String checksumPolicy) { + this.checksumPolicy = checksumPolicy; + } + + } + + public static class Authentication { + + private String username; + + private String password; + + public Authentication() { + } + + public Authentication(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + } +} + diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResource.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResource.java new file mode 100644 index 000000000..95799a320 --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResource.java @@ -0,0 +1,324 @@ +/* + * Copyright 2019-2025 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.deployer.utils; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A {@link Resource} implementation for resolving an artifact via maven coordinates. + *

+ * The {@code MavenResource} class contains + * Maven coordinates for a jar file containing an app/library, or a Bill of Materials pom. + *

+ * To create a new instance, either use {@link Builder} to set the individual fields: + *

+ * new MavenResource.Builder()
+ *     .setGroupId("org.springframework.sample")
+ *     .setArtifactId("some-app")
+ *     .setExtension("jar") //optional
+ *     .setClassifier("exec") //optional
+ *     .setVersion("2.0.0")
+ *     .build()
+ * 
+ * ...or use {@link #parse(String)} to parse the coordinates as a colon delimited string: + * <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version> + *
+ * MavenResource.parse("org.springframework.sample:some-app:2.0.0);
+ * MavenResource.parse("org.springframework.sample:some-app:jar:exec:2.0.0);
+ * 
+ * @author David Turanski + * @author Mark Fisher + * @author Patrick Peralta + * @author Venil Noronha + * @author Ilayaperumal Gopinathan + */ +public final class MavenResource extends AbstractResource { + + /** + * URI Scheme. + */ + public static String URI_SCHEME = "maven"; + + /** + * The default extension for the artifact. + */ + final static String DEFAULT_EXTENSION = "jar"; + + /** + * String representing an empty classifier. + */ + final static String EMPTY_CLASSIFIER = ""; + + /** + * Group ID for artifact; generally this includes the name of the + * organization that generated the artifact. + */ + private final String groupId; + + /** + * Artifact ID; generally this includes the name of the app or library. + */ + private final String artifactId; + + /** + * Extension of the artifact. + */ + private final String extension; + + /** + * Classifier of the artifact. + */ + private final String classifier; + + /** + * Version of the artifact. + */ + private final String version; + + private final MavenArtifactResolver resolver; + + /** + * Construct a {@code MavenResource} object. + * + * @param groupId group ID for artifact + * @param artifactId artifact ID + * @param extension the file extension + * @param classifier artifact classifier - can be null + * @param version artifact version + * @param properties Maven configuration properties + */ + private MavenResource(String groupId, String artifactId, String extension, String classifier, + String version, MavenProperties properties) { + Assert.hasText(groupId, "groupId must not be blank"); + Assert.hasText(artifactId, "artifactId must not be blank"); + Assert.hasText(extension, "extension must not be blank"); + Assert.hasText(version, "version must not be blank"); + this.groupId = groupId; + this.artifactId = artifactId; + this.extension = extension; + this.classifier = classifier == null ? EMPTY_CLASSIFIER : classifier; + this.version = version; + this.resolver = new MavenArtifactResolver(properties != null ? properties : new MavenProperties()); + } + + + public String getGroupId() { + return groupId; + } + + public String getArtifactId() { + return artifactId; + } + + public String getExtension() { + return extension; + } + + public String getClassifier() { + return classifier; + } + + public String getVersion() { + return version; + } + + @Override + public String getDescription() { + return this.toString(); + } + + @Override + public InputStream getInputStream() throws IOException { + return resolver.resolve(this).getInputStream(); + } + + @Override + public File getFile() throws IOException { + return resolver.resolve(this).getFile(); + } + + @Override + public String getFilename() { + return StringUtils.hasLength(classifier) ? + String.format("%s-%s-%s.%s", artifactId, version, classifier, extension) : + String.format("%s-%s.%s", artifactId, version, extension); + } + + @Override + public boolean exists() { + try { + return super.exists(); + } + catch (Exception e) { + // Resource.exists() has no throws clause, so return false + return false; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MavenResource)) { + return false; + } + MavenResource that = (MavenResource) o; + return this.groupId.equals(that.groupId) && + this.artifactId.equals(that.artifactId) && + this.extension.equals(that.extension) && + this.classifier.equals(that.classifier) && + this.version.equals(that.version); + } + + @Override + public int hashCode() { + int result = groupId.hashCode(); + result = 31 * result + artifactId.hashCode(); + result = 31 * result + extension.hashCode(); + if (StringUtils.hasLength(classifier)) { + result = 31 * result + classifier.hashCode(); + } + result = 31 * result + version.hashCode(); + return result; + } + + /** + * Returns the coordinates encoded as + * <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, + * conforming to the Aether convention. + */ + @Override + public String toString() { + return StringUtils.hasLength(classifier) ? + String.format("%s:%s:%s:%s:%s", groupId, artifactId, extension, classifier, version) : + String.format("%s:%s:%s:%s", groupId, artifactId, extension, version); + } + + @Override + public URI getURI() throws IOException { + return URI.create(URI_SCHEME + "://" + toString()); + } + + /** + * Create a {@link MavenResource} for the provided coordinates and default properties. + * + * @param coordinates coordinates encoded as <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, + * conforming to the Aether convention. + * @return the {@link MavenResource} + */ + public static MavenResource parse(String coordinates) { + return parse(coordinates, null); + } + + /** + * Create a {@link MavenResource} for the provided coordinates and properties. + * + * @param coordinates coordinates encoded as <groupId>:<artifactId>[:<extension>[:<classifier>]]:<version>, + * conforming to the Aether convention. + * @param properties the properties for the repositories, proxies, and authentication + * @return the {@link MavenResource} + */ + public static MavenResource parse(String coordinates, MavenProperties properties) { + Assert.hasText(coordinates, "coordinates are required"); + Pattern p = Pattern.compile("([^: ]+):([^: ]+)(:([^: ]*)(:([^: ]+))?)?:([^: ]+)"); + Matcher m = p.matcher(coordinates); + Assert.isTrue(m.matches(), "Bad artifact coordinates " + coordinates + + ", expected format is :[:[:]]:"); + String groupId = m.group(1); + String artifactId = m.group(2); + String extension = StringUtils.hasLength(m.group(4)) ? m.group(4) : DEFAULT_EXTENSION; + String classifier = StringUtils.hasLength(m.group(6)) ? m.group(6) : EMPTY_CLASSIFIER; + String version = m.group(7); + return new MavenResource(groupId, artifactId, extension, classifier, version, properties); + } + + /** + * Get all the available versions on this maven co-ordinate. + * @param coordinates the co-ordinate with the version constraint added. + * Example: org.springframework.cloud.stream.app:http-source-rabbit:[0,) + * @return the list of all the available versions + */ + public List getVersions(String coordinates) { + return this.resolver.getVersions(coordinates); + } + + public static class Builder { + + private String groupId; + + private String artifactId; + + private String extension = DEFAULT_EXTENSION; + + private String classifier = EMPTY_CLASSIFIER; + + private String version; + + private final MavenProperties properties; + + public Builder() { + this(null); + } + + public Builder(MavenProperties properties) { + this.properties = properties; + } + + public Builder groupId(String groupId) { + this.groupId = groupId; + return this; + } + + public Builder artifactId(String artifactId) { + this.artifactId = artifactId; + return this; + } + + public Builder extension(String extension) { + this.extension = extension; + return this; + } + + public Builder classifier(String classifier) { + this.classifier = classifier; + return this; + } + + public Builder version(String version) { + this.version = version; + return this; + } + + public MavenResource build() { + return new MavenResource(groupId, artifactId, extension, classifier, version, properties); + } + } +} + diff --git a/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResourceLoader.java b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResourceLoader.java new file mode 100644 index 000000000..41e25ef0f --- /dev/null +++ b/spring-cloud-function-deployer/src/main/java/org/springframework/cloud/function/deployer/utils/MavenResourceLoader.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019-2025 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.deployer.utils; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A {@link ResourceLoader} that loads {@link MavenResource}s from locations of the format + * {@literal maven://} where the value for "coordinates" conforms to the rules + * described on {@link MavenResource#parse(String)} . + * + * @author Mark Fisher + */ +public class MavenResourceLoader implements ResourceLoader { + + private static final String URI_SCHEME = "maven"; + + private final MavenProperties properties; + + private final ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); + + /** + * Create a {@link MavenResourceLoader} that uses the provided {@link MavenProperties}. + * + * @param properties the {@link MavenProperties} to use when instantiating {@link MavenResource}s + */ + public MavenResourceLoader(MavenProperties properties) { + Assert.notNull(properties, "MavenProperties must not be null"); + this.properties = properties; + } + + /** + * Returns a {@link MavenResource} for the provided location. + * + * @param location the coordinates conforming to the rules described on + * {@link MavenResource#parse(String)}. May optionally be preceded by {@value #URI_SCHEME} + * followed by a colon and zero or more forward slashes, e.g. + * {@literal maven://group:artifact:version} + * @return the {@link MavenResource} + */ + @Override + public Resource getResource(String location) { + Assert.hasText(location, "location is required"); + String coordinates = location.replaceFirst(URI_SCHEME + ":\\/*", ""); + return MavenResource.parse(coordinates, this.properties); + } + + /** + * Returns the {@link ClassLoader} for this ResourceLoader. + */ + @Override + public ClassLoader getClassLoader() { + return this.classLoader; + } + +} diff --git a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java index ce44670c6..8074c7dc2 100644 --- a/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java +++ b/spring-cloud-function-deployer/src/test/java/org/springframework/cloud/function/deployer/FunctionDeployerTests.java @@ -32,9 +32,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.deployer.resource.maven.MavenProperties; import org.springframework.cloud.function.cloudevent.CloudEventMessageBuilder; import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.deployer.utils.MavenProperties; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.messaging.Message; @@ -128,6 +128,7 @@ public void testWithSimplestJar() throws Exception { } @Test + @Disabled public void testWithSimplestJarComponentScanning() throws Exception { String[] args = new String[] { "--spring.cloud.function.location=target/it/simplestjarcs/target/simplestjarcs-1.0.0.RELEASE.jar"}; diff --git a/spring-cloud-function-integration/pom.xml b/spring-cloud-function-integration/pom.xml index 3a1feedeb..7ca98c00a 100644 --- a/spring-cloud-function-integration/pom.xml +++ b/spring-cloud-function-integration/pom.xml @@ -12,7 +12,7 @@ spring-cloud-function-parent org.springframework.cloud - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT diff --git a/spring-cloud-function-integration/src/main/java/org/springframework/cloud/function/integration/dsl/FunctionLookupHelper.java b/spring-cloud-function-integration/src/main/java/org/springframework/cloud/function/integration/dsl/FunctionLookupHelper.java index 262ee1f5e..f4a9637b4 100644 --- a/spring-cloud-function-integration/src/main/java/org/springframework/cloud/function/integration/dsl/FunctionLookupHelper.java +++ b/spring-cloud-function-integration/src/main/java/org/springframework/cloud/function/integration/dsl/FunctionLookupHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2023 the original author or authors. + * Copyright 2023-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.cloud.function.integration.dsl; import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -28,6 +29,7 @@ * The helper class to lookup functions from the catalog in lazy manner and cache their instances. * * @author Artem Bilan + * @author Omer Celik * * @since 4.0.3 */ @@ -72,16 +74,21 @@ private T requireNonNull(Class functionType, String functionDefinition) */ private static Supplier memoize(Supplier delegate) { AtomicReference value = new AtomicReference<>(); + ReentrantLock lock = new ReentrantLock(); return () -> { T val = value.get(); if (val == null) { - synchronized (value) { + try { + lock.lock(); val = value.get(); if (val == null) { val = delegate.get(); value.set(val); } } + finally { + lock.unlock(); + } } return val; }; diff --git a/spring-cloud-function-kotlin/pom.xml b/spring-cloud-function-kotlin/pom.xml index 0d205a652..3427868c6 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 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT @@ -27,13 +27,13 @@ com.amazonaws aws-lambda-java-events - 3.9.0 + 3.14.0 provided com.amazonaws aws-lambda-java-core - 1.2.1 + 1.2.3 provided @@ -86,6 +86,7 @@ kotlin-maven-plugin org.jetbrains.kotlin + 1.9.25 test-compile 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 5954bf4c8..ae28d985d 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 @@ -32,6 +32,8 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.ResolvableType; +import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -57,7 +59,8 @@ public void typeDiscoveryTests() { create(new Class[] { KotlinLambdasConfiguration.class, SimpleConfiguration.class, KotlinComponentFunction.class, - ComponentUppercase.class}); + ComponentUppercase.class, + ComponentWithUnitReturn.class}); FunctionCatalog functionCatalog = this.context.getBean(FunctionCatalog.class); @@ -72,6 +75,10 @@ public void typeDiscoveryTests() { assertThat(kotlinFunction.getInputType()).isEqualTo(String.class); assertThat(kotlinFunction.getOutputType()).isEqualTo(String.class); + FunctionInvocationWrapper componentWithUnitReturn = functionCatalog.lookup("componentWithUnitReturn"); + assertThat(componentWithUnitReturn.isConsumer()).isTrue(); + assertThat(componentWithUnitReturn.getInputType()).isEqualTo(ResolvableType.forClassWithGenerics(Message.class, String.class).getType()); + FunctionInvocationWrapper kotlinConsumer = functionCatalog.lookup("kotlinConsumer"); assertThat(kotlinConsumer.isConsumer()).isTrue(); assertThat(kotlinConsumer.getInputType()).isEqualTo(String.class); diff --git a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinTypeDiscoveryTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinTypeDiscoveryTests.java new file mode 100644 index 000000000..769c0b221 --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/KotlinTypeDiscoveryTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2019-2025 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.kotlin; + + + +import java.lang.reflect.Type; + +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class KotlinTypeDiscoveryTests { + + @Test + public void testOutputInputTypes() { + Type functionType = FunctionTypeUtils.discoverFunctionTypeFromClass(KotlinComponentMessageFunction.class); + Type outputType = FunctionTypeUtils.getOutputType(functionType); + assertThat(FunctionTypeUtils.isMessage(outputType)).isTrue(); + + Type inputType = FunctionTypeUtils.getInputType(functionType); + assertThat(FunctionTypeUtils.isMessage(inputType)).isTrue(); + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/ComponentWithUnitReturn.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/ComponentWithUnitReturn.kt new file mode 100644 index 000000000..93950d9bf --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/ComponentWithUnitReturn.kt @@ -0,0 +1,11 @@ +package org.springframework.cloud.function.kotlin + +import org.springframework.messaging.Message +import org.springframework.stereotype.Component + +@Component +class ComponentWithUnitReturn() : (Message) -> Unit { + override fun invoke(message: Message) { + println(message.payload) + } +} diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentMessageFunction.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentMessageFunction.kt new file mode 100644 index 000000000..7f04cc57c --- /dev/null +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinComponentMessageFunction.kt @@ -0,0 +1,15 @@ +package org.springframework.cloud.function.kotlin + +import org.springframework.messaging.Message +import org.springframework.messaging.MessageHeaders +import org.springframework.messaging.support.MessageBuilder +import org.springframework.stereotype.Component + +import java.util.function.Function + +@Component +class KotlinComponentMessageFunction : (List>) -> List> { + override fun invoke(input: List>): List> { + return input + } +} diff --git a/spring-cloud-function-rsocket/pom.xml b/spring-cloud-function-rsocket/pom.xml index c847fe66f..f66c81904 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 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT diff --git a/spring-cloud-function-rsocket/src/main/resources/banner.txt b/spring-cloud-function-rsocket/src/main/resources/banner.txt new file mode 100644 index 000000000..7a900f5de --- /dev/null +++ b/spring-cloud-function-rsocket/src/main/resources/banner.txt @@ -0,0 +1,22 @@ + ___ ___ _ _ _ +| _ \/ __| ___ __| |_____| |_ ____ _ _ __ _ __ ___ _ _| |_ +| /\__ \/ _ \/ _| / / -_) _| (_-< || | '_ \ '_ \/ _ \ '_| _| +|_|_\|___/\___/\__|_\_\___|\__| /__/\_,_| .__/ .__/\___/_| \__| +| |_ __ _ ___ | |__ ___ ___ _ _ |_| |_| +| ' \/ _` (_-< | '_ \/ -_) -_) ' \ +|_||_\__,_/__/ |_.__/\___\___|_||_| _ + __| |___ _ __ _ _ ___ __ __ _| |_ ___ __| | +/ _` / -_) '_ \ '_/ -_) _/ _` | _/ -_) _` |_ +\__,_\___| .__/_| \___\__\__,_|\__\___\__,_(_) + ___ _ |_| _ _ _ _ _ +|_ _| |_ __ __ _(_) | | _ _ ___| |_ | |__ ___ + | || _| \ V V / | | | | ' \/ _ \ _| | '_ \/ -_) +|___|\__|_ \_/\_/|_|_|_| |_||_\___/\__| |_.__/\___|_ _ + _ _ ___| |___ __ _ ___ ___ __| | __| |_ __ _ _ _| |_(_)_ _ __ _ +| '_/ -_) / -_) _` (_- message) { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean public Function, Message> uppercaseMessage() { - return m -> MessageBuilder.withPayload(m.getPayload().toUpperCase()).copyHeaders(m.getHeaders()).build(); + return m -> MessageBuilder.withPayload(m.getPayload().toUpperCase(Locale.ROOT)).copyHeaders(m.getHeaders()).build(); } @Bean public Function, Flux> uppercaseReactive() { - return flux -> flux.map(v -> v.toUpperCase()); + return flux -> flux.map(v -> v.toUpperCase(Locale.ROOT)); } @Bean public Function>, Flux>> uppercaseReactiveMessage() { - return flux -> flux.map(m -> MessageBuilder.withPayload(m.getPayload().toUpperCase()).copyHeaders(m.getHeaders()).build()); + return flux -> flux.map(m -> MessageBuilder.withPayload(m.getPayload().toUpperCase(Locale.ROOT)).copyHeaders(m.getHeaders()).build()); } @Bean diff --git a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/MessagingTests.java b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/MessagingTests.java index 5238a87ba..726ae7a7a 100644 --- a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/MessagingTests.java +++ b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/MessagingTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.rsocket; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -155,7 +156,7 @@ public void testPojoMessageToPojoViaMessage() { Message message = MessageBuilder.withPayload(p).setHeader("someHeader", "foo").build(); Person result = new Person(); - result.setName(p.getName().toUpperCase()); + result.setName(p.getName().toUpperCase(Locale.ROOT)); rsocketRequesterBuilder.tcp("localhost", port) .route("pojoMessageToPojo") .data(message) @@ -190,7 +191,7 @@ public void testPojoMessageToPojoViaMap() { Map map = jsonMapper.fromJson(message, Map.class); Person result = new Person(); - result.setName(p.getName().toUpperCase()); + result.setName(p.getName().toUpperCase(Locale.ROOT)); rsocketRequesterBuilder.tcp("localhost", port) .route("pojoMessageToPojo") .data(map) @@ -330,7 +331,7 @@ public static class MessagingConfiguration { @Bean public Function pojoToString() { return v -> { - return v.getName().toUpperCase(); + return v.getName().toUpperCase(Locale.ROOT); }; } @@ -355,7 +356,7 @@ public Function, Person> pojoMessageToPojo() { return p -> { assertThat(p.getHeaders().get("someHeader").equals("foo")); Person newPerson = new Person(); - newPerson.setName(p.getPayload().getName().toUpperCase()); + newPerson.setName(p.getPayload().getName().toUpperCase(Locale.ROOT)); return newPerson; }; } @@ -365,7 +366,7 @@ public Function, Message> pojoMessageToPojoMessage() { return p -> { assertThat(p.getHeaders().get("someHeader").equals("foo")); Person newPerson = new Person(); - newPerson.setName(p.getPayload().getName().toUpperCase()); + newPerson.setName(p.getPayload().getName().toUpperCase(Locale.ROOT)); return MessageBuilder.withPayload(newPerson).copyHeaders(p.getHeaders()).setHeader("xyz", "hello").build(); }; } diff --git a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationRoutingTests.java b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationRoutingTests.java index 9fb7a4a1e..0afa56200 100644 --- a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationRoutingTests.java +++ b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationRoutingTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.rsocket; +import java.util.Locale; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -183,7 +184,7 @@ public static class SampleFunctionConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean @@ -193,7 +194,7 @@ public Function, String> uppercaseMessage() { .get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER)).toString().equals("uppercase"); assertThat(msg.getHeaders() .get(FunctionRSocketMessageHandler.RECONCILED_LOOKUP_DESTINATION_HEADER)).toString().equals(RoutingFunction.FUNCTION_NAME); - return msg.getPayload().toUpperCase(); + return msg.getPayload().toUpperCase(Locale.ROOT); }; } @@ -211,7 +212,7 @@ public Function echo() { public Function, Flux> uppercaseReactive() { return flux -> flux.map(v -> { System.out.println("Uppercasing: " + v); - return v.toUpperCase(); + return v.toUpperCase(Locale.ROOT); }); } diff --git a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationTests.java b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationTests.java index f0b942174..05dc9b43e 100644 --- a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationTests.java +++ b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RSocketAutoConfigurationTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.rsocket; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -644,7 +645,7 @@ public static class SampleFunctionConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } @Bean @@ -667,7 +668,7 @@ public Function, Map> echoMap() { public Function, Flux> uppercaseReactive() { return flux -> flux.map(v -> { System.out.println("Uppercasing: " + v); - return v.toUpperCase(); + return v.toUpperCase(Locale.ROOT); }); } diff --git a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RoutingBrokerTests.java b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RoutingBrokerTests.java index 77bbd565a..3b6005681 100644 --- a/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RoutingBrokerTests.java +++ b/spring-cloud-function-rsocket/src/test/java/org/springframework/cloud/function/rsocket/RoutingBrokerTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.rsocket; import java.time.Duration; +import java.util.Locale; import java.util.function.Function; import io.rsocket.broker.client.spring.BrokerMetadata; @@ -139,7 +140,7 @@ public static class SimpleConfiguration { public static class SampleFunctionConfiguration { @Bean public Function uppercase() { - return v -> v.toUpperCase(); + return v -> v.toUpperCase(Locale.ROOT); } } } diff --git a/spring-cloud-function-samples/function-functional-sample-aws/build.gradle b/spring-cloud-function-samples/function-functional-sample-aws/build.gradle index a641c315f..5d1ba6e6b 100644 --- a/spring-cloud-function-samples/function-functional-sample-aws/build.gradle +++ b/spring-cloud-function-samples/function-functional-sample-aws/build.gradle @@ -41,8 +41,8 @@ repositories { ext { springCloudFunctionVersion = "3.0.0.BUILD-SNAPSHOT" - awsLambdaEventsVersion = "2.0.2" - awsLambdaCoreVersion = "1.1.0" + awsLambdaEventsVersion = "3.14.0" + awsLambdaCoreVersion = "1.2.3" } ext['reactor.version'] = "3.1.7.RELEASE" 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 08feb8bfd..23b08b039 100644 --- a/spring-cloud-function-samples/function-functional-sample-aws/pom.xml +++ b/spring-cloud-function-samples/function-functional-sample-aws/pom.xml @@ -15,16 +15,16 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 UTF-8 UTF-8 - 1.0.27.RELEASE - 3.9.0 - 4.1.2-SNAPSHOT + 1.0.31.RELEASE + 3.14.0 + 4.3.0-SNAPSHOT @@ -40,7 +40,7 @@ com.amazonaws aws-lambda-java-core - 1.1.0 + 1.2.3 provided @@ -90,7 +90,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 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 a237eff87..c6392af4e 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 @@ -1,5 +1,6 @@ package example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringBootConfiguration; @@ -25,7 +26,7 @@ public static void main(String[] args) { @Override public void initialize(GenericApplicationContext context) { - Function function = (str) -> str + str.toUpperCase(); + Function function = (str) -> str + str.toUpperCase(Locale.ROOT); context.registerBean("uppercase", FunctionRegistration.class, () -> new FunctionRegistration<>(function).type(FunctionTypeUtils.functionType(String.class, String.class))); 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 52cd298a7..b5e415972 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 - 3.2.4 + 3.5.0-M3 io.spring.sample @@ -15,8 +15,8 @@ Demo project for Spring Cloud Function with custom AWS Lambda runtime using @Bean style - 1.0.27.RELEASE - 4.1.2-SNAPSHOT + 1.0.31.RELEASE + 4.3.0-SNAPSHOT @@ -27,7 +27,7 @@ --> com.amazonaws aws-lambda-java-events - 3.9.0 + 3.14.0 diff --git a/spring-cloud-function-samples/function-sample-aws-custom-bean/src/main/java/com/example/LambdaApplication.java b/spring-cloud-function-samples/function-sample-aws-custom-bean/src/main/java/com/example/LambdaApplication.java index 7fb2ecec8..da84c1287 100644 --- a/spring-cloud-function-samples/function-sample-aws-custom-bean/src/main/java/com/example/LambdaApplication.java +++ b/spring-cloud-function-samples/function-sample-aws-custom-bean/src/main/java/com/example/LambdaApplication.java @@ -1,6 +1,7 @@ package com.example; import java.util.Arrays; +import java.util.Locale; import java.util.function.Consumer; import java.util.function.Function; @@ -30,7 +31,7 @@ public Consumer consume() { public Function uppercase() { return value -> { logger.info("UPPERCASING: " + value); - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); }; } 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 ff25d144e..74adab14d 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 - 3.2.4 + 3.5.0-M3 io.spring.sample @@ -15,8 +15,8 @@ Demo project for Spring Cloud Function with custom AWS Lambda runtime - 1.0.27.RELEASE - 4.1.2-SNAPSHOT + 1.0.31.RELEASE + 4.3.0-SNAPSHOT @@ -42,7 +42,11 @@ org.springframework.boot spring-boot-starter - + + com.amazonaws + aws-lambda-java-core + 1.2.3 + org.springframework.boot spring-boot-starter-test 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 da9d85523..289ba7807 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 @@ -1,5 +1,6 @@ package com.example; +import java.util.Locale; import java.util.function.Function; import org.apache.commons.logging.Log; @@ -24,7 +25,7 @@ public Function uppercase() { if (value.equals("error")) { throw new IllegalArgumentException("Intentional"); } - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); }; } diff --git a/spring-cloud-function-samples/function-sample-aws-native/README.md b/spring-cloud-function-samples/function-sample-aws-native/README.md index 882f2667f..ba0acaf38 100644 --- a/spring-cloud-function-samples/function-sample-aws-native/README.md +++ b/spring-cloud-function-samples/function-sample-aws-native/README.md @@ -31,7 +31,13 @@ Before starting the build, you must clone or download the code in **function-sam ``` 3. Start the container ``` - docker run -dit -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 al2-graalvm19:native-function + docker run -dit -v `pwd`:`pwd` -w `pwd` -v ~/.m2:/root/.m2 al2-graalvm19:native-function + ``` + + or + + ``` + docker run -dit -v $(pwd):$(pwd) -w $(pwd) -v ~/.m2:/root/.m2 al2-graalvm19:native-function ``` 4. In Docker, open the image terminal. diff --git a/spring-cloud-function-samples/function-sample-aws-native/pom.xml b/spring-cloud-function-samples/function-sample-aws-native/pom.xml index acebf4a08..02a23d5c3 100644 --- a/spring-cloud-function-samples/function-sample-aws-native/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws-native/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 oz.native.sample @@ -15,7 +15,7 @@ Sample of AWS with Spring Native 19 - 2023.0.2-SNAPSHOT + 2025.0.0-SNAPSHOT @@ -41,13 +41,12 @@ com.amazonaws aws-lambda-java-events - 3.9.0 + 3.14.0 com.amazonaws aws-lambda-java-core - 1.1.0 - provided + 1.2.3 diff --git a/spring-cloud-function-samples/function-sample-aws-native/src/main/java/com/example/demo/NativeFunctionApplication.java b/spring-cloud-function-samples/function-sample-aws-native/src/main/java/com/example/demo/NativeFunctionApplication.java index 832a72111..399d786db 100644 --- a/spring-cloud-function-samples/function-sample-aws-native/src/main/java/com/example/demo/NativeFunctionApplication.java +++ b/spring-cloud-function-samples/function-sample-aws-native/src/main/java/com/example/demo/NativeFunctionApplication.java @@ -1,15 +1,20 @@ package com.example.demo; +import java.util.Locale; import java.util.function.Function; 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.cloud.function.adapter.aws.AWSLambdaUtils; import org.springframework.context.annotation.Bean; // import org.springframework.cloud.function.context.DefaultMessageRoutingHandler; // import org.springframework.cloud.function.context.MessageRoutingCallback; // import org.springframework.messaging.Message; +import org.springframework.messaging.Message; + +import com.amazonaws.services.lambda.runtime.Context; @SpringBootApplication public class NativeFunctionApplication { @@ -32,10 +37,11 @@ public static void main(String[] args) { // } @Bean - public Function uppercase() { - return v -> { - System.out.println("Uppercasing " + v); - return v.toUpperCase(); + public Function, String> uppercase() { + return message -> { + System.out.println("AWS Context: " + message.getHeaders().get(AWSLambdaUtils.AWS_CONTEXT)); + System.out.println("Uppercasing " + message.getPayload()); + return message.getPayload().toUpperCase(Locale.ROOT); }; } diff --git a/spring-cloud-function-samples/function-sample-aws-routing/build.gradle b/spring-cloud-function-samples/function-sample-aws-routing/build.gradle index a641c315f..5d1ba6e6b 100644 --- a/spring-cloud-function-samples/function-sample-aws-routing/build.gradle +++ b/spring-cloud-function-samples/function-sample-aws-routing/build.gradle @@ -41,8 +41,8 @@ repositories { ext { springCloudFunctionVersion = "3.0.0.BUILD-SNAPSHOT" - awsLambdaEventsVersion = "2.0.2" - awsLambdaCoreVersion = "1.1.0" + awsLambdaEventsVersion = "3.14.0" + awsLambdaCoreVersion = "1.2.3" } ext['reactor.version'] = "3.1.7.RELEASE" 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 405f44026..bd6eeb526 100644 --- a/spring-cloud-function-samples/function-sample-aws-routing/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws-routing/pom.xml @@ -15,16 +15,16 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 UTF-8 UTF-8 - 1.0.27.RELEASE - 2.0.2 - 4.1.2-SNAPSHOT + 1.0.31.RELEASE + 3.14.0 + 4.3.0-SNAPSHOT @@ -45,7 +45,7 @@ com.amazonaws aws-lambda-java-core - 1.1.0 + 1.2.3 provided @@ -95,7 +95,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 false true diff --git a/spring-cloud-function-samples/function-sample-aws-routing/src/main/java/example/FunctionConfiguration.java b/spring-cloud-function-samples/function-sample-aws-routing/src/main/java/example/FunctionConfiguration.java index 29ccdf0d5..937ffe70b 100644 --- a/spring-cloud-function-samples/function-sample-aws-routing/src/main/java/example/FunctionConfiguration.java +++ b/spring-cloud-function-samples/function-sample-aws-routing/src/main/java/example/FunctionConfiguration.java @@ -1,5 +1,6 @@ package example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -19,7 +20,7 @@ public static void main(String[] args) { @Bean public Function uppercase() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } @Bean diff --git a/spring-cloud-function-samples/function-sample-aws-serverless-web-native/pom.xml b/spring-cloud-function-samples/function-sample-aws-serverless-web-native/pom.xml index 401ab3014..b65a90c9f 100644 --- a/spring-cloud-function-samples/function-sample-aws-serverless-web-native/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws-serverless-web-native/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 oz.native.sample @@ -16,7 +16,7 @@ Sample of AWS with Spring Native 21 - 2023.0.2-SNAPSHOT + 2025.0.0-SNAPSHOT @@ -37,12 +37,12 @@ com.amazonaws aws-lambda-java-events - 3.9.0 + 3.14.0 com.amazonaws aws-lambda-java-core - 1.1.0 + 1.2.3 provided @@ -149,4 +149,4 @@
- \ No newline at end of file + 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..f44ad7bd1 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-aws/README.adoc @@ -0,0 +1,38 @@ +This is a basic sample of executing function on AWS. + +You can execute it locally or deploy it to the cloud - https://aws.amazon.com/pm/lambda/[AWS Lambda] + + +To run this app locally please ensure that you have https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html[SAM] (Serverless Application Model) installed on your machine + +---- +> sam build +---- + +and then + +---- +sam local invoke UppercaseFunction --event input.json +---- + +The `input.json` file contains a simple quoted string `"hello"` which will be uppercased and you should see the following in the output + +---- +... +END RequestId: cd119d99-1325-4453-8456-97248dd12cc7 +REPORT RequestId: cd119d99-1325-4453-8456-97248dd12cc7 Init Duration: 1.03 ms Duration: 17740.33 ms Billed Duration: 17741 ms Memory Size: 1024 MB Max Memory Used: 1024 MB +"HELLO" +... +---- + + +To run this app in the cloud, make sure you have AWS Account + +----- + +> mvn clean install +----- + +This will generate the `function-sample-aws-0.0.1-SNAPSHOT-aws.jar` in the `target` directory. + +This is the file you will use to deploy following procedure described https://docs.spring.io/spring-cloud-function/reference/adapters/aws-intro.html[here] diff --git a/spring-cloud-function-samples/function-sample-aws/build.gradle b/spring-cloud-function-samples/function-sample-aws/build.gradle index 1eeb5e5a2..4bc44b2ff 100644 --- a/spring-cloud-function-samples/function-sample-aws/build.gradle +++ b/spring-cloud-function-samples/function-sample-aws/build.gradle @@ -1,19 +1,18 @@ plugins { - id 'org.springframework.cloud.function.aws-lambda.packaging' version '1.0.0' - //id 'java' - id 'org.springframework.boot' version '3.2.0-M2' - //id 'io.spring.dependency-management' version '1.1.3' - //id 'com.github.johnrengelman.shadow' version '8.1.1' - //id 'maven-publish' - // id 'org.springframework.boot.experimental.thin-launcher' version "1.0.31.RELEASE" + id 'java' + id 'org.springframework.boot' version '3.4.4' + id 'io.spring.dependency-management' version '1.1.7' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'maven-publish' + id 'org.springframework.boot.experimental.thin-launcher' version "1.0.31.RELEASE" } group = 'com.example' version = '0.0.1-SNAPSHOT' -//java { -// sourceCompatibility = '17' -//} +java { + sourceCompatibility = '17' +} repositories { mavenCentral() @@ -22,53 +21,53 @@ repositories { } ext { - set('springCloudVersion', "2023.0.0-M1") + set('springCloudVersion', "2024.0.1") } -//assemble.dependsOn = [thinJar, shadowJar] - -//publishing { -// publications { -// maven(MavenPublication) { -// from components.java -// versionMapping { -// usage('java-api') { -// fromResolutionOf('runtimeClasspath') -// } -// usage('java-runtime') { -// fromResolutionResult() -// } -// } -// } -// } -//} +assemble.dependsOn = [thinJar, shadowJar] -//shadowJar.mustRunAfter thinJar +publishing { + publications { + maven(MavenPublication) { + from components.java + versionMapping { + usage('java-api') { + fromResolutionOf('runtimeClasspath') + } + usage('java-runtime') { + fromResolutionResult() + } + } + } + } +} +shadowJar.mustRunAfter thinJar -//import com.github.jengelman.gradle.plugins.shadow.transformers.* +import com.github.jengelman.gradle.plugins.shadow.transformers.* -//shadowJar { - //archiveClassifier = 'aws' - //manifest { - // inheritFrom(project.tasks.thinJar.manifest) - //} - // Required for Spring - //mergeServiceFiles() - //append 'META-INF/spring.handlers' - //append 'META-INF/spring.schemas' - //append 'META-INF/spring.tooling' - //append 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' - //append 'META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports' - //transform(PropertiesFileTransformer) { - // paths = ['META-INF/spring.factories'] - // mergeStrategy = "append" - //} -//} +shadowJar { + archiveClassifier = 'aws' + manifest { + inheritFrom(project.tasks.thinJar.manifest) + } + // Required for Spring + mergeServiceFiles() + append 'META-INF/spring.handlers' + append 'META-INF/spring.schemas' + append 'META-INF/spring.tooling' + append 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' + append 'META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports' + transform(PropertiesFileTransformer) { + paths = ['META-INF/spring.factories'] + mergeStrategy = "append" + } +} dependencies { implementation 'org.springframework.boot:spring-boot-starter' - implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws:4.1.0-SNAPSHOT' + implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws' + implementation 'org.springframework.cloud:spring-cloud-function-context' testImplementation 'org.springframework.boot:spring-boot-starter-test' } diff --git a/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.jar b/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.jar index 033e24c4c..a4b76b953 100644 Binary files a/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.jar and b/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.jar differ diff --git a/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.properties b/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.properties index 9f4197d5f..9355b4155 100644 --- a/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.properties +++ b/spring-cloud-function-samples/function-sample-aws/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/spring-cloud-function-samples/function-sample-aws/gradlew b/spring-cloud-function-samples/function-sample-aws/gradlew index fcb6fca14..f5feea6d6 100755 --- a/spring-cloud-function-samples/function-sample-aws/gradlew +++ b/spring-cloud-function-samples/function-sample-aws/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,7 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/spring-cloud-function-samples/function-sample-aws/gradlew.bat b/spring-cloud-function-samples/function-sample-aws/gradlew.bat index 6689b85be..9b42019c7 100644 --- a/spring-cloud-function-samples/function-sample-aws/gradlew.bat +++ b/spring-cloud-function-samples/function-sample-aws/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -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. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -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. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/spring-cloud-function-samples/function-sample-aws/input.json b/spring-cloud-function-samples/function-sample-aws/input.json new file mode 100644 index 000000000..84ed78b69 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-aws/input.json @@ -0,0 +1 @@ +"hello" \ 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 8b8d95f98..ce7d91e1a 100644 --- a/spring-cloud-function-samples/function-sample-aws/pom.xml +++ b/spring-cloud-function-samples/function-sample-aws/pom.xml @@ -15,16 +15,16 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 UTF-8 UTF-8 - 1.0.29.RELEASE - 3.9.0 - 4.1.2-SNAPSHOT + 1.0.31.RELEASE + 3.14.0 + 4.3.0-SNAPSHOT @@ -40,7 +40,7 @@ com.amazonaws aws-lambda-java-core - 1.1.0 + 1.2.3 provided 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..e1bc3f4fe 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 @@ -1,5 +1,6 @@ package example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -24,7 +25,7 @@ public Function uppercase() { throw new RuntimeException("Intentional exception"); } else { - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } }; } diff --git a/spring-cloud-function-samples/function-sample-aws/src/main/resources/META-INF/mask.keys b/spring-cloud-function-samples/function-sample-aws/src/main/resources/META-INF/mask.keys new file mode 100644 index 000000000..9d06eaa76 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-aws/src/main/resources/META-INF/mask.keys @@ -0,0 +1,4 @@ +routeKey +rawQueryString +path +resource \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-aws/template.yml b/spring-cloud-function-samples/function-sample-aws/template.yml new file mode 100644 index 000000000..0672ef02e --- /dev/null +++ b/spring-cloud-function-samples/function-sample-aws/template.yml @@ -0,0 +1,41 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example function as lambda deployment + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + UppercaseFunction: + Type: AWS::Serverless::Function + Properties: +# AutoPublishAlias: bcn + FunctionName: uppercase + Handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest + Runtime: java17 + SnapStart: + ApplyOn: PublishedVersions + CodeUri: . + MemorySize: 1024 + Policies: AWSLambdaBasicExecutionRole + Timeout: 30 + Environment: + Variables: + MAIN_CLASS: example.FunctionConfiguration + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 20000 + PayloadFormatVersion: '1.0' + +Outputs: + UppercaseFunctionApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/uppercase' + Export: + Name: UppercaseAPI + + diff --git a/spring-cloud-function-samples/function-sample-azure-blob-trigger/pom.xml b/spring-cloud-function-samples/function-sample-azure-blob-trigger/pom.xml index ee5ac6b0c..07e51a406 100644 --- a/spring-cloud-function-samples/function-sample-azure-blob-trigger/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure-blob-trigger/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 @@ -16,7 +16,7 @@ Demo project for Spring Boot 17 - 1.0.28.RELEASE + 1.0.31.RELEASE com.example.azure.di.azureblobtriggerdemo.AzureBlobTriggerDemoApplication diff --git a/spring-cloud-function-samples/function-sample-azure-blob-trigger/src/main/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplication.java b/spring-cloud-function-samples/function-sample-azure-blob-trigger/src/main/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplication.java index ce0ab0212..9ef38a8a6 100644 --- a/spring-cloud-function-samples/function-sample-azure-blob-trigger/src/main/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplication.java +++ b/spring-cloud-function-samples/function-sample-azure-blob-trigger/src/main/java/com/example/azure/di/azureblobtriggerdemo/AzureBlobTriggerDemoApplication.java @@ -16,6 +16,7 @@ package com.example.azure.di.azureblobtriggerdemo; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -31,6 +32,6 @@ public static void main(String[] args) { @Bean public Function uppercase() { - return payload -> new String(payload).toUpperCase().getBytes(); + return payload -> new String(payload).toUpperCase(Locale.ROOT).getBytes(); } } diff --git a/spring-cloud-function-samples/function-sample-azure-http-trigger-gradle/src/main/java/org/scf/azure/gradle/GradleDemoApplication.java b/spring-cloud-function-samples/function-sample-azure-http-trigger-gradle/src/main/java/org/scf/azure/gradle/GradleDemoApplication.java index 3d5902df8..7ae83b90c 100644 --- a/spring-cloud-function-samples/function-sample-azure-http-trigger-gradle/src/main/java/org/scf/azure/gradle/GradleDemoApplication.java +++ b/spring-cloud-function-samples/function-sample-azure-http-trigger-gradle/src/main/java/org/scf/azure/gradle/GradleDemoApplication.java @@ -1,5 +1,6 @@ package org.scf.azure.gradle; +import java.util.Locale; import java.util.Optional; import java.util.function.Function; @@ -49,11 +50,11 @@ public Function, String> uppercase() { return message -> { ExecutionContext context = (ExecutionContext) message.getHeaders().get(AzureFunctionUtil.EXECUTION_CONTEXT); - String updatedPayload = message.getPayload().toUpperCase(); + String updatedPayload = message.getPayload().toUpperCase(Locale.ROOT); context.getLogger().info("Azure Test: " + updatedPayload); - return message.getPayload().toUpperCase(); + return message.getPayload().toUpperCase(Locale.ROOT); }; } diff --git a/spring-cloud-function-samples/function-sample-azure-http-trigger/pom.xml b/spring-cloud-function-samples/function-sample-azure-http-trigger/pom.xml index b00d8adbc..546cfe3ac 100644 --- a/spring-cloud-function-samples/function-sample-azure-http-trigger/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure-http-trigger/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 @@ -18,7 +18,7 @@ 17 - 1.0.28.RELEASE + 1.0.31.RELEASE com.example.azure.di.httptriggerdemo.HttpTriggerDemoApplication diff --git a/spring-cloud-function-samples/function-sample-azure-http-trigger/src/main/java/com/example/azure/di/httptriggerdemo/HttpTriggerDemoApplication.java b/spring-cloud-function-samples/function-sample-azure-http-trigger/src/main/java/com/example/azure/di/httptriggerdemo/HttpTriggerDemoApplication.java index fc7bd8ea3..21d550d6f 100644 --- a/spring-cloud-function-samples/function-sample-azure-http-trigger/src/main/java/com/example/azure/di/httptriggerdemo/HttpTriggerDemoApplication.java +++ b/spring-cloud-function-samples/function-sample-azure-http-trigger/src/main/java/com/example/azure/di/httptriggerdemo/HttpTriggerDemoApplication.java @@ -16,6 +16,7 @@ package com.example.azure.di.httptriggerdemo; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -32,7 +33,7 @@ public Function echo() { @Bean public Function uppercase() { - return payload -> payload.toUpperCase(); + return payload -> payload.toUpperCase(Locale.ROOT); } @Bean diff --git a/spring-cloud-function-samples/function-sample-azure-kafka-trigger/pom.xml b/spring-cloud-function-samples/function-sample-azure-kafka-trigger/pom.xml index 903b02adc..c50972179 100644 --- a/spring-cloud-function-samples/function-sample-azure-kafka-trigger/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure-kafka-trigger/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 @@ -16,8 +16,8 @@ Demo project for Spring Boot 17 - 4.1.0-SNAPSHOT - 1.0.28.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE example.KafkaTriggerDemoApplication diff --git a/spring-cloud-function-samples/function-sample-azure-kafka-trigger/src/main/java/example/KafkaTriggerDemoApplication.java b/spring-cloud-function-samples/function-sample-azure-kafka-trigger/src/main/java/example/KafkaTriggerDemoApplication.java index bb01e7c8a..235c42e02 100644 --- a/spring-cloud-function-samples/function-sample-azure-kafka-trigger/src/main/java/example/KafkaTriggerDemoApplication.java +++ b/spring-cloud-function-samples/function-sample-azure-kafka-trigger/src/main/java/example/KafkaTriggerDemoApplication.java @@ -15,6 +15,7 @@ */ package example; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -45,7 +46,7 @@ public Function, String> uppercase(JsonMapper mapper) { Map valueMap = mapper.fromJson(kafkaEntity.getValue(), Map.class); if (valueMap != null) { valueMap.forEach((k, v) -> valueMap.put(k, - v != null && v instanceof String ? ((String) v).toUpperCase() : null)); + v != null && v instanceof String ? ((String) v).toUpperCase(Locale.ROOT) : null)); return mapper.toString(valueMap); } } diff --git a/spring-cloud-function-samples/function-sample-azure-time-trigger/pom.xml b/spring-cloud-function-samples/function-sample-azure-time-trigger/pom.xml index 9a41b52a6..50ef9d30f 100644 --- a/spring-cloud-function-samples/function-sample-azure-time-trigger/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure-time-trigger/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 @@ -16,7 +16,7 @@ Demo project for Spring Boot 17 - 1.0.28.RELEASE + 1.0.31.RELEASE com.example.azure.di.timetriggerdemo.TimeTriggerDemoApplication diff --git a/spring-cloud-function-samples/function-sample-azure-time-trigger/src/main/java/com/example/azure/di/timetriggerdemo/TimeTriggerDemoApplication.java b/spring-cloud-function-samples/function-sample-azure-time-trigger/src/main/java/com/example/azure/di/timetriggerdemo/TimeTriggerDemoApplication.java index e3e10ddc8..c61aedda4 100644 --- a/spring-cloud-function-samples/function-sample-azure-time-trigger/src/main/java/com/example/azure/di/timetriggerdemo/TimeTriggerDemoApplication.java +++ b/spring-cloud-function-samples/function-sample-azure-time-trigger/src/main/java/com/example/azure/di/timetriggerdemo/TimeTriggerDemoApplication.java @@ -16,6 +16,7 @@ package com.example.azure.di.timetriggerdemo; +import java.util.Locale; import java.util.function.Consumer; import com.microsoft.azure.functions.ExecutionContext; @@ -40,7 +41,7 @@ public static void main(String[] args) { public Consumer> uppercase() { return message -> { String timeInfo = message.getPayload(); - String value = timeInfo.toUpperCase(); + String value = timeInfo.toUpperCase(Locale.ROOT); logger.info("Timer is triggered with TimeInfo: " + value); diff --git a/spring-cloud-function-samples/function-sample-azure-web/pom.xml b/spring-cloud-function-samples/function-sample-azure-web/pom.xml index 107d1ccee..d7cdb88ac 100644 --- a/spring-cloud-function-samples/function-sample-azure-web/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure-web/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 @@ -19,7 +19,7 @@ 17 - 1.0.28.RELEASE + 1.0.31.RELEASE 4.1.0-SNAPSHOT @@ -44,7 +44,7 @@ ${spring-cloud-function-adapter-azure-web.version} - + org.springframework.boot spring-boot-starter-web diff --git a/spring-cloud-function-samples/function-sample-azure/pom.xml b/spring-cloud-function-samples/function-sample-azure/pom.xml index 2df3fcbdc..5c3d6f582 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 - 3.2.4 + 3.5.0-M3 @@ -28,7 +28,7 @@ example.Config 1.21.0 2.1.0 - 1.0.27.RELEASE + 1.0.31.RELEASE 17 UTF-8 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 b40249436..97ae9b2e7 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 @@ -16,6 +16,7 @@ package example; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -54,7 +55,7 @@ public Function, String> uppercase(JsonMapper mapper) { Map map = mapper.fromJson(value, Map.class); if(map != null) - map.forEach((k, v) -> map.put(k, v != null ? v.toUpperCase() : null)); + map.forEach((k, v) -> map.put(k, v != null ? v.toUpperCase(Locale.ROOT) : null)); if(context != null) context.getLogger().info(new StringBuilder().append("Function: ") @@ -73,12 +74,12 @@ public Function, String> uppercase(JsonMapper mapper) { @Bean public Function, Mono> uppercaseReactive() { - return mono -> mono.map(value -> value.toUpperCase()); + return mono -> mono.map(value -> value.toUpperCase(Locale.ROOT)); } @Bean public Function, Flux> echoStream() { - return flux -> flux.map(value -> value.toUpperCase()); + return flux -> flux.map(value -> value.toUpperCase(Locale.ROOT)); } } 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 index 8f72a80bc..744a46b6d 100644 --- 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 @@ -17,6 +17,7 @@ package example; import java.util.List; +import java.util.Locale; import com.microsoft.azure.functions.ExecutionContext; import com.microsoft.azure.functions.HttpMethod; @@ -54,7 +55,7 @@ protected String postProcessFluxFunctionResult(List rawInputs, Object fu ) { functionResult .doFirst(() -> executionContext.getLogger().info("BEGIN echo post-processing work ...")) - .mapNotNull((v) -> v.toString().toUpperCase()) + .mapNotNull((v) -> v.toString().toUpperCase(Locale.ROOT)) .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/.gitignore b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.gitignore deleted file mode 100644 index 549e00a2a..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -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-sample-cloudevent-rsocket/.mvn/wrapper/MavenWrapperDownloader.java b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/MavenWrapperDownloader.java deleted file mode 100644 index e76d1f324..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/MavenWrapperDownloader.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2007-present 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. - */ -import java.net.*; -import java.io.*; -import java.nio.channels.*; -import java.util.Properties; - -public class MavenWrapperDownloader { - - private static final String WRAPPER_VERSION = "0.5.6"; - /** - * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. - */ - private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" - + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; - - /** - * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to - * use instead of the default one. - */ - private static final String MAVEN_WRAPPER_PROPERTIES_PATH = - ".mvn/wrapper/maven-wrapper.properties"; - - /** - * Path where the maven-wrapper.jar will be saved to. - */ - private static final String MAVEN_WRAPPER_JAR_PATH = - ".mvn/wrapper/maven-wrapper.jar"; - - /** - * Name of the property which should be used to override the default download url for the wrapper. - */ - private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; - - public static void main(String args[]) { - System.out.println("- Downloader started"); - File baseDirectory = new File(args[0]); - System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); - - // If the maven-wrapper.properties exists, read it and check if it contains a custom - // wrapperUrl parameter. - File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); - String url = DEFAULT_DOWNLOAD_URL; - if(mavenWrapperPropertyFile.exists()) { - FileInputStream mavenWrapperPropertyFileInputStream = null; - try { - mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); - Properties mavenWrapperProperties = new Properties(); - mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); - url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); - } catch (IOException e) { - System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); - } finally { - try { - if(mavenWrapperPropertyFileInputStream != null) { - mavenWrapperPropertyFileInputStream.close(); - } - } catch (IOException e) { - // Ignore ... - } - } - } - System.out.println("- Downloading from: " + url); - - File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); - if(!outputFile.getParentFile().exists()) { - if(!outputFile.getParentFile().mkdirs()) { - System.out.println( - "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); - } - } - System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); - try { - downloadFileFromURL(url, outputFile); - System.out.println("Done"); - System.exit(0); - } catch (Throwable e) { - System.out.println("- Error downloading"); - e.printStackTrace(); - System.exit(1); - } - } - - private static void downloadFileFromURL(String urlString, File destination) throws Exception { - if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { - String username = System.getenv("MVNW_USERNAME"); - char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); - Authenticator.setDefault(new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(username, password); - } - }); - } - URL website = new URL(urlString); - ReadableByteChannel rbc; - rbc = Channels.newChannel(website.openStream()); - FileOutputStream fos = new FileOutputStream(destination); - fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); - fos.close(); - rbc.close(); - } - -} diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/maven-wrapper.jar b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/maven-wrapper.jar deleted file mode 100644 index 2cc7d4a55..000000000 Binary files a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/maven-wrapper.jar and /dev/null differ diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/maven-wrapper.properties b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/maven-wrapper.properties deleted file mode 100644 index 642d572ce..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/.mvn/wrapper/maven-wrapper.properties +++ /dev/null @@ -1,2 +0,0 @@ -distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip -wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/README.adoc b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/README.adoc deleted file mode 100644 index ba154a464..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/README.adoc +++ /dev/null @@ -1,61 +0,0 @@ -## Examples of Cloud Events with Spring via RSocket and Apache Kafka - -### Introduction -The current example uses spring-cloud-function framework as its core which allows users to only worry about functional aspects of -their requirement while taking care-off non-functional aspects. For more information on Spring Cloud Function please visit -our https://spring.io/projects/spring-cloud-function[project page]. - -The example consists of a Spring boot configuration class -https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/DemoApplication.java[DemoApplication] -which contains a sample function which you can interact with following via RSocket and Apache Kafka. - -### From RSocket to Apache Kafka - -While very similar to https://github.com/spring-cloud/spring-cloud-function/tree/master/spring-cloud-function-samples/function-sample-cloudevent-stream[spring-cloud-function-stream] example -there are few interesting variants here worth discussing. -Here we’re introducing a different delivery mechanism. But what really makes it even more interesting is the fact that unlike Apache Kafka or AMQP there is no protocol -binding defined for RSocket. So we will communicate Cloud Event in a structured-mode where the entire event is encoded into some type of structure (e.g., JSON). - -Few implementation details are also defer in this example from the others. However these details are not relevant in any way to Cloud Event, rather -demonstration of other mechanisms you may chose to write your code. For example we’ll be using `Consumer` instead of a `Function` and will be manually -sending an output message using `StreamBridge` component provided by Spring Cloud Stream framework. - -So, here is our application code - -``` -@Bean -public Consumer hire(StreamBridge streamBridge) { - return person -> { - Employee employee = new Employee(person); - streamBridge.send("hire-out-0", CloudEventMessageBuilder.withData(employee) - .setSource("http://spring.io/rsocket") - .setId("1234567890") - .build()); - }; -} -``` -Note how we’re utiliziing CloudEventMessageBuilder to generate output Message as Cloud Event. - -What we will be sending over RSocket is structured representation of Cloud Event: -``` -String payload = "{\n" + - " \"specversion\" : \"1.0\",\n" + - " \"type\" : \"org.springframework\",\n" + - " \"source\" : \"https://spring.io/\",\n" + - " \"id\" : \"A234-1234-1234\",\n" + - " \"datacontenttype\" : \"application/json\",\n" + - " \"data\" : {\n" + - " \"firstName\" : \"John\",\n" + - " \"lastName\" : \"Doe\"\n" + - " }\n" + - "}"; -``` -So, the entire Cloud Event is represented as JSON sent over RSocket to the hire() function. - -``` -rsocketRequesterBuilder.tcp("localhost", 55555) - .route("hire") // target function - .data(payload). // data we're sending - .send() -``` -You can run the demo using https://github.com/spring-cloud/spring-cloud-function/blob/master/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/test/java/io/spring/cloudevent/DemoApplicationTests.java[DemoApplicationTests] \ No newline at end of file diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/mvnw b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/mvnw deleted file mode 100755 index a16b5431b..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/mvnw +++ /dev/null @@ -1,310 +0,0 @@ -#!/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 /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="`which 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/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - else - jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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" - else - wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$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 \ - -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-cloudevent-rsocket/mvnw.cmd b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/mvnw.cmd deleted file mode 100644 index c8d43372c..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/mvnw.cmd +++ /dev/null @@ -1,182 +0,0 @@ -@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 "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" -if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\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/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" - -FOR /F "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%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.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 "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" -if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\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% - -exit /B %ERROR_CODE% diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/pom.xml b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/pom.xml deleted file mode 100644 index b308192bf..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/pom.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - 4.0.0 - io.spring.sample - function-sample-cloudevent-rsocket - 0.0.1-SNAPSHOT - function-sample-cloudevent-rsocket - Demo project for Spring Boot - - - org.springframework.boot - spring-boot-starter-parent - 3.2.4 - - - - - 4.1.2-SNAPSHOT - 1.0.27.RELEASE - - - - - - - org.springframework.cloud - spring-cloud-function-rsocket - - - - - - - org.springframework.cloud - spring-cloud-stream-binder-kafka - 4.1.0-SNAPSHOT - - - - io.projectreactor - reactor-test - test - - - org.springframework.boot - spring-boot-starter-test - test - - - org.junit.vintage - junit-vintage-engine - - - - - - - - - org.springframework.cloud - spring-cloud-function-dependencies - ${spring-cloud-function.version} - pom - import - - - - - - - - - org.apache.maven.plugins - maven-deploy-plugin - - true - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.springframework.boot.experimental - spring-boot-thin-layout - ${wrapper.version} - - - - - maven-surefire-plugin - - - **/*Tests.java - **/*Test.java - - - **/Abstract*.java - - - - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/release - - false - - - - - - spring-snapshots - Spring Snapshots - https://repo.spring.io/libs-snapshot-local - - true - - - false - - - - spring-milestones - Spring Milestones - https://repo.spring.io/libs-milestone-local - - false - - - - spring-releases - Spring Releases - https://repo.spring.io/libs-release-local - - false - - - - - - diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/DemoApplication.java b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/DemoApplication.java deleted file mode 100644 index a42bf0b79..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/DemoApplication.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.spring.cloudevent; - -import java.util.function.Consumer; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.cloud.function.cloudevent.CloudEventMessageBuilder; -import org.springframework.cloud.stream.function.StreamBridge; -import org.springframework.context.annotation.Bean; - -@SpringBootApplication -public class DemoApplication { - - public static void main(String[] args) throws Exception { - SpringApplication.run(DemoApplication.class, args); - } - - @Bean - public Consumer hire(StreamBridge streamBridge) { - return person -> { - Employee employee = new Employee(person); - streamBridge.send("hire-out-0", CloudEventMessageBuilder.withData(employee) - .setSource("http://spring.io/rsocket") - .setId("1234567890") - .build()); - }; - } -} diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/Employee.java b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/Employee.java deleted file mode 100644 index e1f04615e..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/Employee.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.spring.cloudevent; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Random; - -public class Employee { - - private Person person; - - private int id; - - public Employee() { - - } - - public Employee(Person person) { - this.person = person; - this.id = new Random().nextInt(1000); - } - - public Person getPerson() { - return person; - } - - public void setPerson(Person person) { - this.person = person; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getMessage() { - return "Employee " + id + " was hired on " + new SimpleDateFormat("dd-MM-yyyy").format(new Date()); - } -} diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/Person.java b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/Person.java deleted file mode 100644 index 99ded7514..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/java/io/spring/cloudevent/Person.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.spring.cloudevent; - -public class Person { - - private String firstName; - - private String lastName; - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } -} diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/resources/application.properties b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/main/resources/application.properties deleted file mode 100644 index e69de29bb..000000000 diff --git a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/test/java/io/spring/cloudevent/DemoApplicationTests.java b/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/test/java/io/spring/cloudevent/DemoApplicationTests.java deleted file mode 100644 index 87e232a57..000000000 --- a/spring-cloud-function-samples/function-sample-cloudevent-rsocket/src/test/java/io/spring/cloudevent/DemoApplicationTests.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.spring.cloudevent; - -import java.net.InetSocketAddress; -import java.net.Socket; -import java.util.Collections; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.TimeUnit; - -import org.apache.kafka.clients.admin.KafkaAdminClient; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ConditionEvaluationResult; -import org.junit.jupiter.api.extension.ExecutionCondition; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.function.cloudevent.CloudEventMessageUtils; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.messaging.Message; -import org.springframework.messaging.rsocket.RSocketRequester; -import org.springframework.util.MimeTypeUtils; - - -@SpringBootTest(properties = {"spring.rsocket.server.port=55551"}) -@ExtendWith(DemoApplicationTests.TestRule.class) -public class DemoApplicationTests { - - ArrayBlockingQueue> queue = new ArrayBlockingQueue<>(1000); - - @Autowired - private RSocketRequester.Builder rsocketRequesterBuilder; - - @Test - public void test() throws Exception { - String payload = "{\n" + - " \"specversion\" : \"1.0\",\n" + - " \"type\" : \"org.springframework\",\n" + - " \"source\" : \"https://spring.io/\",\n" + - " \"id\" : \"A234-1234-1234\",\n" + - " \"datacontenttype\" : \"application/json\",\n" + - " \"data\" : {\n" + - " \"firstName\" : \"John\",\n" + - " \"lastName\" : \"Doe\"\n" + - " }\n" + - "}"; - - this.rsocketRequesterBuilder.tcp("localhost", 55551) - .route("hire") - .metadata("{\"content-type\":\"application/cloudevents+json\"}", MimeTypeUtils.APPLICATION_JSON) - .data(payload) - .send() - .subscribe(); - - Message resultFromKafka = queue.poll(2000, TimeUnit.MILLISECONDS); - System.out.println("Result Message: " + resultFromKafka); - System.out.println("Cloud Event 'specversion': " + CloudEventMessageUtils.getSpecVersion(resultFromKafka)); - System.out.println("Cloud Event 'source': " + CloudEventMessageUtils.getSource(resultFromKafka)); - System.out.println("Cloud Event 'id': " + CloudEventMessageUtils.getId(resultFromKafka)); - System.out.println("Cloud Event 'type': " + CloudEventMessageUtils.getType(resultFromKafka)); - } - - @KafkaListener(id = "test", topics = "hire-out-0", clientIdPrefix = "cloudEvents") - public void listen(Message message) { - queue.add(message); - } - - public static class TestRule implements ExecutionCondition { - @Override - public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { - try { - Socket socket = new Socket(); - socket.connect(new InetSocketAddress("localhost", 9092)); - socket.close(); - } - catch (Exception e) { - System.out.println("Kafka is not available on localhost:9092"); - return ConditionEvaluationResult.disabled("Kafka is not available on localhost, default port"); - } - - return ConditionEvaluationResult.enabled("All is good"); - } - } -} diff --git a/spring-cloud-function-samples/function-sample-cloudevent-sdk/pom.xml b/spring-cloud-function-samples/function-sample-cloudevent-sdk/pom.xml index 01f452d4c..ec6b7296d 100644 --- a/spring-cloud-function-samples/function-sample-cloudevent-sdk/pom.xml +++ b/spring-cloud-function-samples/function-sample-cloudevent-sdk/pom.xml @@ -11,13 +11,13 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.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 ad81fc64f..9e689bcb7 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 - 3.2.4 + 3.5.0-M3 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE @@ -28,13 +28,13 @@ org.springframework.cloud spring-cloud-stream - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT org.springframework.cloud spring-cloud-stream-binder-rabbit - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT @@ -42,7 +42,7 @@ org.springframework.cloud spring-cloud-stream-binder-kafka - 4.1.0-SNAPSHOT + 4.2.0-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample-cloudevent/pom.xml b/spring-cloud-function-samples/function-sample-cloudevent/pom.xml index 8cf117ce1..0f8ce4d06 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 - 3.2.4 + 3.5.0-M3 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-samples/function-sample-functional-aws-routing/build.gradle b/spring-cloud-function-samples/function-sample-functional-aws-routing/build.gradle index a641c315f..5d1ba6e6b 100644 --- a/spring-cloud-function-samples/function-sample-functional-aws-routing/build.gradle +++ b/spring-cloud-function-samples/function-sample-functional-aws-routing/build.gradle @@ -41,8 +41,8 @@ repositories { ext { springCloudFunctionVersion = "3.0.0.BUILD-SNAPSHOT" - awsLambdaEventsVersion = "2.0.2" - awsLambdaCoreVersion = "1.1.0" + awsLambdaEventsVersion = "3.14.0" + awsLambdaCoreVersion = "1.2.3" } ext['reactor.version'] = "3.1.7.RELEASE" 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 20a773279..a38b45817 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,16 +15,16 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 UTF-8 UTF-8 - 1.0.27.RELEASE - 2.0.2 - 4.1.2-SNAPSHOT + 1.0.31.RELEASE + 3.14.0 + 4.3.0-SNAPSHOT @@ -49,7 +49,7 @@ com.amazonaws aws-lambda-java-core - 1.1.0 + 1.2.3 provided @@ -99,7 +99,7 @@ org.apache.maven.plugins maven-shade-plugin - 3.2.4 + 3.6.0 false true 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 554133da0..3ae200a36 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 @@ -1,5 +1,6 @@ package example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -23,7 +24,7 @@ public static void main(String[] args) { } public Function uppercase() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } public Function reverse() { 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 636c91006..27ca74906 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 - 3.2.4 + 3.5.0-M3 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 fdcf74dd9..e41273bb6 100644 --- a/spring-cloud-function-samples/function-sample-gcp-http/pom.xml +++ b/spring-cloud-function-samples/function-sample-gcp-http/pom.xml @@ -15,19 +15,19 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT org.springframework.cloud spring-cloud-function-adapter-gcp - 4.1.0-SNAPSHOT + ${spring-cloud-function.version} diff --git a/spring-cloud-function-samples/function-sample-gcp-http/src/main/java/com/example/CloudFunctionMain.java b/spring-cloud-function-samples/function-sample-gcp-http/src/main/java/com/example/CloudFunctionMain.java index dc5f213ab..c5fe46794 100644 --- a/spring-cloud-function-samples/function-sample-gcp-http/src/main/java/com/example/CloudFunctionMain.java +++ b/spring-cloud-function-samples/function-sample-gcp-http/src/main/java/com/example/CloudFunctionMain.java @@ -16,6 +16,7 @@ package com.example; +import java.util.Locale; import java.util.function.Function; import org.springframework.boot.SpringApplication; @@ -31,6 +32,6 @@ public static void main(String[] args) { @Bean public Function function() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-samples/function-sample-gcp-http/src/test/java/com/example/FunctionSampleGcpIntegrationTest.java b/spring-cloud-function-samples/function-sample-gcp-http/src/test/java/com/example/FunctionSampleGcpIntegrationTest.java index 2ebd4f819..69f813c1a 100644 --- a/spring-cloud-function-samples/function-sample-gcp-http/src/test/java/com/example/FunctionSampleGcpIntegrationTest.java +++ b/spring-cloud-function-samples/function-sample-gcp-http/src/test/java/com/example/FunctionSampleGcpIntegrationTest.java @@ -28,7 +28,7 @@ public class FunctionSampleGcpIntegrationTest { private TestRestTemplate rest = new TestRestTemplate(); - @Test + //@Test public void testSample() throws IOException, InterruptedException { try (LocalServerTestSupport.ServerProcess process = LocalServerTestSupport.startServer(CloudFunctionMain.class)) { String result = rest.postForObject("http://localhost:8080/", "Hello", String.class); 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 b7bdacf71..6177fb82a 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 - 3.2.4 + 3.5.0-M3 com.example.grpc @@ -16,7 +16,7 @@ function-sample-grpc-cloudevent Demo project for Spring Boot - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT 1.55.1 diff --git a/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java b/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java index 83d7bcc33..fe64801ff 100644 --- a/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java +++ b/spring-cloud-function-samples/function-sample-grpc-cloudevent/src/main/java/com/example/grpc/demo/DemoGrpcApplication.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -55,7 +56,7 @@ public static void main(String[] args) throws Exception { @Bean public Function, Message> uppercase() { return message -> { - return MessageBuilder.withPayload(message.getPayload().toUpperCase()) + return MessageBuilder.withPayload(message.getPayload().toUpperCase(Locale.ROOT)) .copyHeaders(message.getHeaders()) .setHeader("uppercased", "true") .build(); 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 1e6d80ed5..17957d360 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 - 3.2.4 + 3.5.0-M3 @@ -31,17 +31,12 @@ org.springframework.cloud spring-cloud-function-kotlin - 4.1.1-SNAPSHOT + 4.3.0-SNAPSHOT org.springframework.cloud spring-cloud-function-web - 4.1.0-SNAPSHOT - - - org.springframework.cloud - spring-cloud-function-context - 4.1.0-SNAPSHOT + 4.3.0-SNAPSHOT org.springframework.boot @@ -72,6 +67,7 @@ org.jetbrains.kotlin kotlin-maven-plugin + 1.9.25 -Xjsr305=strict diff --git a/spring-cloud-function-samples/function-sample-pof/pom.xml b/spring-cloud-function-samples/function-sample-pof/pom.xml index 509961617..94fdc6c08 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 - 3.2.4 + 3.5.0-M3 @@ -21,7 +21,7 @@ UTF-8 UTF-8 3.1.2.RELEASE - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT @@ -62,7 +62,7 @@ org.springframework.boot.experimental spring-boot-thin-layout - 1.0.27.RELEASE + 1.0.31.RELEASE diff --git a/spring-cloud-function-samples/function-sample-pojo/pom.xml b/spring-cloud-function-samples/function-sample-pojo/pom.xml index 479b19b74..7c87fd510 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 - 3.2.4 + 3.5.0-M3 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE diff --git a/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java b/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java index 52fe3dbf5..1e6aa9b44 100644 --- a/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java +++ b/spring-cloud-function-samples/function-sample-pojo/src/main/java/com/example/SampleApplication.java @@ -17,6 +17,7 @@ package com.example; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; @@ -71,11 +72,11 @@ class Foo { } public String lowercase() { - return this.value.toLowerCase(); + return this.value.toLowerCase(Locale.ROOT); } public String uppercase() { - return this.value.toUpperCase(); + return this.value.toUpperCase(Locale.ROOT); } public String getValue() { 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 e6fc8addb..f80aa0f6e 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 - 3.2.4 + 3.5.0-M3 @@ -20,7 +20,7 @@ UTF-8 UTF-8 17 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT @@ -71,7 +71,7 @@ org.springframework.boot.experimental spring-boot-thin-layout - 1.0.27.RELEASE + 1.0.31.RELEASE 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 fd4c9a590..feb1322c4 100644 --- a/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml +++ b/spring-cloud-function-samples/function-sample-supplier-exporter/pom.xml @@ -14,12 +14,12 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT diff --git a/spring-cloud-function-samples/function-sample/pom.xml b/spring-cloud-function-samples/function-sample/pom.xml index b5d12ca4b..6bbd1cc10 100644 --- a/spring-cloud-function-samples/function-sample/pom.xml +++ b/spring-cloud-function-samples/function-sample/pom.xml @@ -14,13 +14,13 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 - 4.1.2-SNAPSHOT - 1.0.27.RELEASE + 4.3.0-SNAPSHOT + 1.0.31.RELEASE 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 f26aec07f..533db8629 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 @@ -17,6 +17,7 @@ package com.example; import java.time.Duration; +import java.util.Locale; import java.util.function.Function; import java.util.function.Supplier; @@ -38,17 +39,17 @@ public static void main(String[] args) throws Exception { @Bean public Function uppercase() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } @Bean public Function, Integer> uppercaseMessage() { - return value -> value.getPayload().toUpperCase().length(); + return value -> value.getPayload().toUpperCase(Locale.ROOT).length(); } @Bean public Function, Flux> lowercase() { - return flux -> flux.map(value -> value.toLowerCase()); + return flux -> flux.map(value -> value.toLowerCase(Locale.ROOT)); } @Bean diff --git a/spring-cloud-function-samples/pom.xml b/spring-cloud-function-samples/pom.xml index 5f12bd9b7..b1c97c611 100644 --- a/spring-cloud-function-samples/pom.xml +++ b/spring-cloud-function-samples/pom.xml @@ -11,7 +11,7 @@ org.springframework.cloud spring-cloud-function-parent - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT @@ -30,9 +30,7 @@ function-sample-gcp-background function-sample-cloudevent function-sample-cloudevent-stream - function-sample-cloudevent-rsocket function-sample-cloudevent-sdk - function-sample-grpc-cloudevent diff --git a/spring-cloud-function-samples/scf-aws-day1/pom.xml b/spring-cloud-function-samples/scf-aws-day1/pom.xml index 57291c762..ed1579b46 100644 --- a/spring-cloud-function-samples/scf-aws-day1/pom.xml +++ b/spring-cloud-function-samples/scf-aws-day1/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 oz.spring @@ -15,10 +15,10 @@ Template project for creating function that can be deployed as AWS Lambda 17 - 2023.0.2-SNAPSHOT + 2025.0.0-SNAPSHOT 1.0.31.RELEASE - 3.9.0 - 1.1.0 + 3.14.0 + 1.2.3 @@ -37,7 +37,7 @@ com.amazonaws aws-lambda-java-core - 1.1.0 + ${aws-lambda-java-core.version} provided diff --git a/spring-cloud-function-samples/scf-aws-day1/src/main/java/oz/spring/aws/functions/FunctionConfiguration.java b/spring-cloud-function-samples/scf-aws-day1/src/main/java/oz/spring/aws/functions/FunctionConfiguration.java index f019c0fc6..aeacc8950 100644 --- a/spring-cloud-function-samples/scf-aws-day1/src/main/java/oz/spring/aws/functions/FunctionConfiguration.java +++ b/spring-cloud-function-samples/scf-aws-day1/src/main/java/oz/spring/aws/functions/FunctionConfiguration.java @@ -1,5 +1,6 @@ package oz.spring.aws.functions; +import java.util.Locale; import java.util.function.Function; import org.springframework.context.annotation.Bean; @@ -10,6 +11,6 @@ public class FunctionConfiguration { @Bean public Function uppercase() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-samples/scf-aws-routing/pom.xml b/spring-cloud-function-samples/scf-aws-routing/pom.xml index 8136f0a2c..37d3aed0b 100644 --- a/spring-cloud-function-samples/scf-aws-routing/pom.xml +++ b/spring-cloud-function-samples/scf-aws-routing/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.2.4 + 3.5.0-M3 oz.spring @@ -15,10 +15,10 @@ Template project for creating function that can be deployed as AWS Lambda 17 - 2023.0.2-SNAPSHOT + 2025.0.0-SNAPSHOT 1.0.31.RELEASE - 3.9.0 - 1.1.0 + 3.14.0 + 1.2.3 @@ -37,7 +37,7 @@ com.amazonaws aws-lambda-java-core - 1.1.0 + ${aws-lambda-java-core.version} provided diff --git a/spring-cloud-function-samples/scf-aws-routing/src/main/java/oz/spring/aws/functions/FunctionRoutingConfiguration.java b/spring-cloud-function-samples/scf-aws-routing/src/main/java/oz/spring/aws/functions/FunctionRoutingConfiguration.java index cfbfa9c3e..500b1b05c 100644 --- a/spring-cloud-function-samples/scf-aws-routing/src/main/java/oz/spring/aws/functions/FunctionRoutingConfiguration.java +++ b/spring-cloud-function-samples/scf-aws-routing/src/main/java/oz/spring/aws/functions/FunctionRoutingConfiguration.java @@ -1,5 +1,6 @@ package oz.spring.aws.functions; +import java.util.Locale; import java.util.function.Function; import org.springframework.context.annotation.Bean; @@ -10,7 +11,7 @@ public class FunctionRoutingConfiguration { @Bean public Function lowercase() { - return value -> value.toLowerCase(); + return value -> value.toLowerCase(Locale.ROOT); } @Bean diff --git a/spring-cloud-function-web/pom.xml b/spring-cloud-function-web/pom.xml index e5177d860..abb64910f 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 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT @@ -66,6 +66,11 @@ awaitility test + + org.apache.httpcomponents.client5 + httpclient5 + test + 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 98666d756..4a19c8b76 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 @@ -179,9 +179,9 @@ public Mono> get(ServerWebExchange request) { private FunctionWrapper wrapper(ServerWebExchange request) { FunctionInvocationWrapper function = (FunctionInvocationWrapper) request .getAttribute(WebRequestConstants.HANDLER); - HttpHeaders headers = HttpHeaders.writableHttpHeaders(request.getRequest().getHeaders()); + HttpHeaders headers = new HttpHeaders(request.getRequest().getHeaders()); headers.set("uri", request.getRequest().getURI().toString()); - FunctionWrapper wrapper = new FunctionWrapper(function); + FunctionWrapper wrapper = new FunctionWrapper(function, null); wrapper.setHeaders(headers); wrapper.getParams().addAll(request.getRequest().getQueryParams()); String argument = (String) request.getAttribute(WebRequestConstants.ARGUMENT); diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java index 4696c1d52..29f31acf5 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java @@ -244,7 +244,7 @@ public RouterFunction functionEndpoints() { FunctionInvocationWrapper funcWrapper = extract(request); Class outputType = funcWrapper == null ? Object.class : FunctionTypeUtils.getRawType(FunctionTypeUtils.getGenericType(funcWrapper.getOutputType())); - FunctionWrapper wrapper = new FunctionWrapper(funcWrapper); + FunctionWrapper wrapper = new FunctionWrapper(funcWrapper, null); Mono> stream = request.bodyToMono(String.class) .flatMap(content -> (Mono>) FunctionWebRequestProcessingHelper.processRequest(wrapper, content, false, functionHttpProperties.getIgnoredHeaders(), functionHttpProperties.getRequestOnlyHeaders())); @@ -270,7 +270,7 @@ public RouterFunction functionEndpoints() { return ServerResponse.ok().body(result, outputType); } else { - FunctionWrapper wrapper = new FunctionWrapper(funcWrapper); + FunctionWrapper wrapper = new FunctionWrapper(funcWrapper, null); wrapper.setHeaders(request.headers().asHttpHeaders()); String argument = (String) request.attribute(WebRequestConstants.ARGUMENT).get(); 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 6af5bfb7e..c13c9ca29 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 @@ -16,6 +16,7 @@ package org.springframework.cloud.function.web.util; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -233,15 +234,14 @@ private static Object postProcessResult(Object result, boolean isMessage) { else if (result instanceof Mono) { result = ((Mono) result).map(v -> postProcessResult(v, isMessage)); } - else if (result instanceof Message) { - if (((Message) result).getPayload() instanceof byte[]) { - String str = new String((byte[]) ((Message) result).getPayload()); - result = MessageBuilder.withPayload(str).copyHeaders(((Message) result).getHeaders()).build(); + else if (result instanceof Message messageResult) { + if (messageResult.getPayload() instanceof byte[]) { + //String str = new String((byte[]) messageResult.getPayload()); + result = MessageBuilder.withPayload(messageResult.getPayload()).copyHeaders(((Message) result).getHeaders()).build(); } } - - if (result instanceof byte[]) { - result = new String((byte[]) result); + else if (result instanceof byte[]) { + result = new String((byte[]) result, StandardCharsets.UTF_8); } return result; } diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java index d9b6af26d..627cf9508 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWrapper.java @@ -39,16 +39,6 @@ public class FunctionWrapper { private final String method; - /** - * - * @param function instance of {@link FunctionInvocationWrapper} - * @deprecated since 4.0.4 in favor of the constructor that takes Http method as second argument. - */ - @Deprecated - public FunctionWrapper(FunctionInvocationWrapper function) { - this(function, null); - } - public FunctionWrapper(FunctionInvocationWrapper function, String method) { this.function = function; this.method = method; 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 3c8fbb56f..d4382fe6f 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 @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import org.springframework.http.HttpHeaders; @@ -44,6 +45,7 @@ public final class HeaderUtils { static { IGNORED.add(MessageHeaders.ID, ""); IGNORED.add(HttpHeaders.CONTENT_LENGTH, "0"); + IGNORED.add(HttpHeaders.TRANSFER_ENCODING, "*"); // Headers that would typically be added by a downstream client REQUEST_ONLY.add(HttpHeaders.ACCEPT, ""); REQUEST_ONLY.add(HttpHeaders.CONTENT_LENGTH, ""); @@ -59,7 +61,7 @@ public static HttpHeaders fromMessage(MessageHeaders headers, List ignor HttpHeaders result = new HttpHeaders(); for (String name : headers.keySet()) { Object value = headers.get(name); - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); if (!IGNORED.containsKey(name) && !ignoredHeders.contains(name)) { Collection values = multi(value); for (Object object : values) { @@ -80,7 +82,7 @@ public static HttpHeaders sanitize(HttpHeaders request, List ignoredHede HttpHeaders result = new HttpHeaders(); for (String name : request.keySet()) { List value = request.get(name); - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); if (!IGNORED.containsKey(name) && !REQUEST_ONLY.containsKey(name) && !ignoredHeders.contains(name) && !requestOnlyHeaders.contains(name)) { result.put(name, value); } @@ -97,10 +99,10 @@ public static MessageHeaders fromHttp(HttpHeaders headers) { Map map = new LinkedHashMap<>(); for (String name : headers.keySet()) { Collection values = multi(headers.get(name)); - name = name.toLowerCase(); + name = name.toLowerCase(Locale.ROOT); Object value = values == null ? null : (values.size() == 1 ? values.iterator().next() : values); - if (name.toLowerCase().equals(HttpHeaders.CONTENT_TYPE.toLowerCase())) { + if (name.toLowerCase(Locale.ROOT).equals(HttpHeaders.CONTENT_TYPE.toLowerCase(Locale.ROOT))) { name = MessageHeaders.CONTENT_TYPE; } map.put(name, value); 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 660910910..63f6a3a36 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 @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -300,13 +301,13 @@ public static class TestConfiguration { @PostMapping({ "/uppercase", "/transform", "/post/more" }) public Flux uppercase(@RequestBody List flux) { return Flux.fromIterable(flux).log() - .map(value -> "[" + value.trim().toUpperCase() + "]"); + .map(value -> "[" + value.trim().toUpperCase(Locale.ROOT) + "]"); } @PostMapping({ "/alt" }) public Mono> alt(@RequestBody List flux) { Publisher result = Flux.fromIterable(flux) - .map(value -> "[" + value.trim().toUpperCase() + "]"); + .map(value -> "[" + value.trim().toUpperCase(Locale.ROOT) + "]"); return Flux.from(result).log() .then(Mono.fromSupplier(() -> ResponseEntity.ok(result))); } @@ -314,12 +315,12 @@ public Mono> alt(@RequestBody List flux) { @PostMapping("/upFoos") public Flux upFoos(@RequestBody List list) { return Flux.fromIterable(list).log() - .map(value -> new Foo(value.getValue().trim().toUpperCase())); + .map(value -> new Foo(value.getValue().trim().toUpperCase(Locale.ROOT))); } @GetMapping("/uppercase/{id}") public Mono> uppercaseGet(@PathVariable String id) { - return Mono.just(id).map(value -> "[" + value.trim().toUpperCase() + "]") + return Mono.just(id).map(value -> "[" + value.trim().toUpperCase(Locale.ROOT) + "]") .flatMap(body -> Mono.just(ResponseEntity.ok(body))); } @@ -339,7 +340,7 @@ public Mono> entity(@PathVariable Integer id) { public Flux> maps( @RequestBody List> flux) { return Flux.fromIterable(flux).map(value -> { - value.put("value", value.get("value").trim().toUpperCase()); + value.put("value", value.get("value").trim().toUpperCase(Locale.ROOT)); return value; }); } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/mvc/MvcRestApplicationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/mvc/MvcRestApplicationTests.java index 1d051ae3b..0a8f73bf4 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/mvc/MvcRestApplicationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/mvc/MvcRestApplicationTests.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import org.junit.jupiter.api.BeforeEach; @@ -289,18 +290,18 @@ public static class TestConfiguration { @PostMapping({ "/uppercase", "/transform", "/post/more" }) public Flux uppercase(@RequestBody List flux) { return Flux.fromIterable(flux).log() - .map(value -> "[" + value.trim().toUpperCase() + "]"); + .map(value -> "[" + value.trim().toUpperCase(Locale.ROOT) + "]"); } @PostMapping("/upFoos") public Flux upFoos(@RequestBody List list) { return Flux.fromIterable(list).log() - .map(value -> new Foo(value.getValue().trim().toUpperCase())); + .map(value -> new Foo(value.getValue().trim().toUpperCase(Locale.ROOT))); } @GetMapping("/uppercase/{id}") public Mono uppercaseGet(@PathVariable String id) { - return Mono.just(id).map(value -> "[" + value.trim().toUpperCase() + "]"); + return Mono.just(id).map(value -> "[" + value.trim().toUpperCase(Locale.ROOT) + "]"); } @GetMapping("/wrap/{id}") @@ -318,7 +319,7 @@ public Mono> entity(@PathVariable Integer id) { public Flux> maps( @RequestBody List> flux) { return Flux.fromIterable(flux).map(value -> { - value.put("value", value.get("value").trim().toUpperCase()); + value.put("value", value.get("value").trim().toUpperCase(Locale.ROOT)); return value; }); } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ExplicitNonFunctionalTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ExplicitNonFunctionalTests.java index a25046754..a9a0f9bbc 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ExplicitNonFunctionalTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ExplicitNonFunctionalTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.test; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -26,6 +27,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.web.reactive.server.WebTestClient; @@ -35,7 +37,7 @@ */ @SpringBootTest({ "spring.main.web-application-type=REACTIVE", "spring.functional.enabled=false" }) -@AutoConfigureWebTestClient +@AutoConfigureWebTestClient(timeout = "10000") @DirtiesContext public class ExplicitNonFunctionalTests { @@ -44,17 +46,19 @@ public class ExplicitNonFunctionalTests { @Test public void words() throws Exception { - this.client.post().uri("/").body(Mono.just("foo"), String.class).exchange() + this.client + .post().uri("/").body(Mono.just("foo"), String.class).exchange() .expectStatus().isOk().expectBody(String.class).isEqualTo("FOO"); } @SpringBootConfiguration @EnableAutoConfiguration + @Configuration(proxyBeanMethods = false) protected static class TestConfiguration implements Function { @Override public String apply(String value) { - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } 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 15ecb84c7..f72500d42 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 @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -111,7 +112,7 @@ protected static class ApplicationConfiguration Function, Message> uppercase() { return value -> { headers.putAll(value.getHeaders()); - return MessageBuilder.withPayload(value.getPayload().getName().toUpperCase()) + return MessageBuilder.withPayload(value.getPayload().getName().toUpperCase(Locale.ROOT)) .copyHeaders(value.getHeaders()).build(); }; } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalTests.java index 00883f4c8..434f2fb80 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.test; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -50,7 +51,7 @@ protected static class TestConfiguration implements Function { @Override public String apply(String value) { - return value.toUpperCase(); + return value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputListTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputListTests.java index d30b7df71..28dfc0f76 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputListTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputListTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.test; import java.util.List; +import java.util.Locale; import java.util.function.Function; import java.util.stream.Collectors; @@ -54,7 +55,7 @@ protected static class TestConfiguration implements Function, Foo> { @Override public Foo apply(List value) { - return new Foo(value.stream().map(foo -> foo.getValue().toUpperCase()) + return new Foo(value.stream().map(foo -> foo.getValue().toUpperCase(Locale.ROOT)) .collect(Collectors.joining())); } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputSetTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputSetTests.java index 066959957..e5f426834 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputSetTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/FunctionalWithInputSetTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.test; import java.time.Duration; +import java.util.Locale; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -62,7 +63,7 @@ protected static class TestConfiguration implements Function, Foo> { @Override public Foo apply(Set value) { - return new Foo(value.stream().map(foo -> foo.getValue().toUpperCase()) + return new Foo(value.stream().map(foo -> foo.getValue().toUpperCase(Locale.ROOT)) .collect(Collectors.joining())); } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/HeadersToMessageTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/HeadersToMessageTests.java index 710843883..c0da259cc 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/HeadersToMessageTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/HeadersToMessageTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.test; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -57,7 +58,7 @@ protected static class TestConfiguration @Override public Message apply(Message request) { Message message = MessageBuilder - .withPayload(request.getPayload().toUpperCase()) + .withPayload(request.getPayload().toUpperCase(Locale.ROOT)) .setHeader("X-Content-Type", "application/xml") .setHeader("foo", "bar").build(); return message; diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ImplicitNonFunctionalTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ImplicitNonFunctionalTests.java index 28e067430..ac26fd14f 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ImplicitNonFunctionalTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/ImplicitNonFunctionalTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.test; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -54,7 +55,7 @@ protected static class TestConfiguration { @Bean public Function uppercase() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/PojoTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/PojoTests.java index b622e8a0f..44efc5ff8 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/PojoTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/PojoTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.function.test; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -60,7 +61,7 @@ protected static class TestConfiguration implements Function { @Override public Foo apply(Foo value) { - return new Foo(value.getValue().toUpperCase()); + return new Foo(value.getValue().toUpperCase(Locale.ROOT)); } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpGetIntegrationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpGetIntegrationTests.java index 2505c6a53..effaa7c30 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpGetIntegrationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/flux/HttpGetIntegrationTests.java @@ -22,6 +22,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; @@ -278,7 +279,7 @@ public Function, Flux> reverse() { @Bean({ "uppercase", "post/more" }) public Function, Flux> uppercase() { return flux -> flux.log() - .map(value -> "(" + value.trim().toUpperCase() + ")"); + .map(value -> "(" + value.trim().toUpperCase(Locale.ROOT) + ")"); } @Bean 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 0598c06f7..eb4d61ab8 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 @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -44,6 +45,7 @@ import org.springframework.cloud.function.web.RestApplication; import org.springframework.cloud.function.web.flux.HttpPostIntegrationTests.ApplicationConfiguration; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.RequestEntity; @@ -398,6 +400,7 @@ private String sse(String... values) { } @EnableAutoConfiguration + @Configuration(proxyBeanMethods = false) public static class ApplicationConfiguration { private List list = new ArrayList<>(); @@ -410,37 +413,37 @@ public static void main(String[] args) throws Exception { @Bean({ "uppercase", "transform", "post/more" }) public Function, Flux> uppercase() { return flux -> flux.log() - .map(value -> "(" + value.trim().toUpperCase() + ")"); + .map(value -> "(" + value.trim().toUpperCase(Locale.ROOT) + ")"); } @Bean public Function bareUppercase() { - return value -> "(" + value.trim().toUpperCase() + ")"; + return value -> "(" + value.trim().toUpperCase(Locale.ROOT) + ")"; } @Bean public Function, Message> messages() { return value -> MessageBuilder - .withPayload("(" + value.getPayload().trim().toUpperCase() + ")") + .withPayload("(" + value.getPayload().trim().toUpperCase(Locale.ROOT) + ")") .copyHeaders(value.getHeaders()).build(); } @Bean public Function>, Flux>> headers() { return flux -> flux.map(value -> MessageBuilder - .withPayload("(" + value.getPayload().trim().toUpperCase() + ")") + .withPayload("(" + value.getPayload().trim().toUpperCase(Locale.ROOT) + ")") .setHeader("foo", "bar").build()); } @Bean public Function, Flux> upFoos() { return flux -> flux.log() - .map(value -> new Foo(value.getValue().trim().toUpperCase())); + .map(value -> new Foo(value.getValue().trim().toUpperCase(Locale.ROOT))); } @Bean public Function bareUpFoos() { - return value -> new Foo(value.getValue().trim().toUpperCase()); + return value -> new Foo(value.getValue().trim().toUpperCase(Locale.ROOT)); } @Bean @@ -464,7 +467,7 @@ public Function, Flux> doubler() { @Bean public Function>, Flux>> maps() { return flux -> flux.map(value -> { - value.put("value", value.get("value").trim().toUpperCase()); + value.put("value", value.get("value").trim().toUpperCase(Locale.ROOT)); return value; }); } @@ -473,14 +476,13 @@ public Function>, Flux>> maps() @Qualifier("foos") public Function qualifier() { return value -> { - return new Foo("[" + value.trim().toUpperCase() + "]"); + return new Foo("[" + value.trim().toUpperCase(Locale.ROOT) + "]"); }; } @Bean public Consumer> updates() { return flux -> flux.subscribe(value -> { - System.out.println(); this.list.add(value); }); } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerMVCTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerMVCTests.java index e6aa8efd4..fa749cd07 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerMVCTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializerMVCTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.web.function; import java.net.URI; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -66,12 +67,12 @@ protected static class ApplicationConfiguration { @Bean public Function uppercase() { - return s -> s.toUpperCase(); + return s -> s.toUpperCase(Locale.ROOT); } @Bean public Function lowercase() { - return s -> s.toLowerCase(); + return s -> s.toLowerCase(Locale.ROOT); } @Bean 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 4429d0ce4..66189a9de 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 @@ -19,6 +19,7 @@ import java.net.URI; import java.time.Duration; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -125,7 +126,6 @@ public void testGetWithtFunction() throws Exception { TestRestTemplate testRestTemplate = new TestRestTemplate(); ResponseEntity response = testRestTemplate .getForEntity(new URI("http://localhost:" + port + "/reverse/stressed"), String.class); - System.out.println(); assertThat(response.getBody()).isEqualTo("desserts"); } @@ -196,11 +196,11 @@ public Supplier supplier() { } public Function uppercase() { - return s -> s.toUpperCase(); + return s -> s.toUpperCase(Locale.ROOT); } public Function lowercase() { - return s -> s.toLowerCase(); + return s -> s.toLowerCase(Locale.ROOT); } public Function reverse() { diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/HeadersResponseMappingTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/HeadersResponseMappingTests.java new file mode 100644 index 000000000..462c263ae --- /dev/null +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/function/HeadersResponseMappingTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.web.function; + + +import java.net.URI; +import java.util.Locale; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HeadersResponseMappingTests { + + // see https://github.com/spring-cloud/spring-cloud-function/issues/1220 + @Test + public void test_1220() throws Exception { + ConfigurableApplicationContext context = SpringApplication.run(ApplicationConfiguration.class, + "--server.port=0"); + TestRestTemplate testRestTemplate = new TestRestTemplate(); + String port = context.getEnvironment().getProperty("local.server.port"); + ResponseEntity response = testRestTemplate.postForEntity( + new URI("http://localhost:" + port + "/uppercase"), new Person("John", "Doe"), String.class); + assertThat(response.getBody()).isEqualTo("JOHN"); + } + + static record Person(String firstName, String lastName) { + } + + @SpringBootApplication + protected static class ApplicationConfiguration { + + @Bean + public Function uppercase() { + return s -> s.firstName().toUpperCase(Locale.ROOT); + } + + } +} 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 dfbe5413a..0994efb7d 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 @@ -17,6 +17,7 @@ package org.springframework.cloud.function.web.function; import java.net.URI; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -69,7 +70,7 @@ protected static class Issue274Configuration { @Bean public Function echo() { - return s -> s.toUpperCase(); + return s -> s.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/DefaultRouteTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/DefaultRouteTests.java index e441eb5d9..e5510722c 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/DefaultRouteTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/DefaultRouteTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.web.mvc; import java.net.URI; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Disabled; @@ -77,7 +78,7 @@ protected static class TestConfiguration { @Bean public Function, Flux> uppercase() { - return flux -> flux.map(value -> value.toUpperCase()); + return flux -> flux.map(value -> value.toUpperCase(Locale.ROOT)); } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpGetIntegrationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpGetIntegrationTests.java index d1629d0ec..e0092fbab 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpGetIntegrationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/HttpGetIntegrationTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; @@ -292,7 +293,7 @@ public Function, Flux> reverse() { @Bean({ "uppercase", "post/more" }) public Function, Flux> uppercase() { return flux -> flux.log() - .map(value -> "(" + value.trim().toUpperCase() + ")"); + .map(value -> "(" + value.trim().toUpperCase(Locale.ROOT) + ")"); } @Bean 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 a12c46dcf..b3d9b2aa4 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 @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -348,37 +349,37 @@ public static void main(String[] args) throws Exception { @Bean({ "uppercase", "transform", "post/more" }) public Function, Flux> uppercase() { return flux -> flux.log() - .map(value -> "(" + value.trim().toUpperCase() + ")"); + .map(value -> "(" + value.trim().toUpperCase(Locale.ROOT) + ")"); } @Bean public Function bareUppercase() { - return value -> "(" + value.trim().toUpperCase() + ")"; + return value -> "(" + value.trim().toUpperCase(Locale.ROOT) + ")"; } @Bean public Function, Message> messages() { return value -> MessageBuilder - .withPayload("(" + value.getPayload().trim().toUpperCase() + ")") + .withPayload("(" + value.getPayload().trim().toUpperCase(Locale.ROOT) + ")") .copyHeaders(value.getHeaders()).build(); } @Bean public Function>, Flux>> headers() { return flux -> flux.map(value -> MessageBuilder - .withPayload("(" + value.getPayload().trim().toUpperCase() + ")") + .withPayload("(" + value.getPayload().trim().toUpperCase(Locale.ROOT) + ")") .setHeader("foo", "bar").build()); } @Bean public Function, Flux> upFoos() { return flux -> flux.log() - .map(value -> new Foo(value.getValue().trim().toUpperCase())); + .map(value -> new Foo(value.getValue().trim().toUpperCase(Locale.ROOT))); } @Bean public Function bareUpFoos() { - return value -> new Foo(value.getValue().trim().toUpperCase()); + return value -> new Foo(value.getValue().trim().toUpperCase(Locale.ROOT)); } @Bean @@ -394,7 +395,7 @@ public Function, Flux> doubler() { @Bean public Function>, Flux>> maps() { return flux -> flux.map(value -> { - value.put("value", value.get("value").trim().toUpperCase()); + value.put("value", value.get("value").trim().toUpperCase(Locale.ROOT)); return value; }); } @@ -402,7 +403,7 @@ public Function>, Flux>> maps() @Bean @Qualifier("foos") public Function qualifier() { - return value -> new Foo("[" + value.trim().toUpperCase() + "]"); + return value -> new Foo("[" + value.trim().toUpperCase(Locale.ROOT) + "]"); } @Bean diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/MultipartFileTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/MultipartFileTests.java index 5b35a86c4..e1fde9f2c 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/MultipartFileTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/MultipartFileTests.java @@ -18,6 +18,7 @@ import java.net.URI; import java.util.List; +import java.util.Locale; import java.util.function.Function; import org.junit.jupiter.api.Test; @@ -93,7 +94,7 @@ protected static class TestConfiguration { @Bean public Function uppercase() { return value -> { - return value.getOriginalFilename().toUpperCase(); + return value.getOriginalFilename().toUpperCase(Locale.ROOT); }; } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/RoutingFunctionTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/RoutingFunctionTests.java index e032af809..b107d96bf 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/RoutingFunctionTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/mvc/RoutingFunctionTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.web.mvc; import java.net.URI; +import java.util.Locale; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; @@ -210,7 +211,7 @@ public Function echo() { public Function, Flux> fluxuppercase() { return v -> v.map(s -> { System.out.println(s); - return s.toUpperCase(); + return s.toUpperCase(Locale.ROOT); }); } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationIntegrationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationIntegrationTests.java index 73f768b4d..7d272f701 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationIntegrationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationIntegrationTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.function.Function; import org.apache.commons.logging.Log; @@ -94,7 +95,7 @@ public static class ApplicationConfiguration { @Bean public Function uppercase() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationWithRetriesIntegrationTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationWithRetriesIntegrationTests.java index e7957e32b..e15b571f8 100644 --- a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationWithRetriesIntegrationTests.java +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/web/source/FunctionAutoConfigurationWithRetriesIntegrationTests.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.function.Function; import org.apache.commons.logging.Log; @@ -96,7 +97,7 @@ public static class ApplicationConfiguration { @Bean public Function uppercase() { - return value -> value.toUpperCase(); + return value -> value.toUpperCase(Locale.ROOT); } } diff --git a/spring-cloud-starter-function-web/pom.xml b/spring-cloud-starter-function-web/pom.xml index e78565737..0930a48b9 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 - 4.1.2-SNAPSHOT + 4.3.0-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 151c1443e..5cd2ca58e 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 - 4.1.2-SNAPSHOT + 4.3.0-SNAPSHOT spring-cloud-starter-function-webflux spring-cloud-starter-function-webflux