diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index b509d952..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,108 +0,0 @@ -version: 2 -jobs: - build: - working_directory: ~/fn-java-fdk - machine: - java: - version: oraclejdk8 - environment: - # store_artifacts doesn't shell substitute so the variable - # definitions are duplicated in those steps too. - FDK_ARTIFACT_DIR: /tmp/artifacts/fdk - TEST_ARTIFACT_DIR: /tmp/artifacts/tests - STAGING_DIR: /tmp/staging-repository - steps: - - checkout - - run: - name: Set release to latest branch version - command: | - git checkout origin/${CIRCLE_BRANCH} release.version - echo next release version is $(cat release.version) - - run: - name: Determine the release version - command: ./.circleci/update-versions.sh - - run: - name: Build and deploy locally - command: | - rm -rf $STAGING_DIR - mkdir -p $STAGING_DIR - mvn deploy -DaltDeploymentRepository=localStagingDir::default::file://"$STAGING_DIR" - - store_test_results: - path: runtime/target/surefire-reports - - store_test_results: - path: testing/target/surefire-reports - - - run: - name: Copy FDK artifacts to upload folder - command: | - mkdir -p "$FDK_ARTIFACT_DIR" - cp -a api/target/*.jar "$FDK_ARTIFACT_DIR" - cp -a runtime/target/*.jar "$FDK_ARTIFACT_DIR" - - store_artifacts: - name: Upload FDK artifacts - path: /tmp/artifacts/fdk - - - run: - name: Update Docker to latest - command: ./.circleci/install-docker.sh - - - run: - name: Login to Docker - command: docker login -u $DOCKER_USER -p $DOCKER_PASS - - - run: - name: Build fn-java-fdk Docker build image - command: | - cd build-image - ./docker-build.sh -t fnproject/fn-java-fdk-build:$(cat ../release.version) . - - - run: - name: Build fn-java-fdk-jdk9 Docker build image - command: | - cd build-image - ./docker-build.sh -f Dockerfile-jdk9 -t fnproject/fn-java-fdk-build:jdk9-$(cat ../release.version) . - - run: - name: Build fn-java-fdk Docker runtime image - command: | - cd runtime - docker build -t fnproject/fn-java-fdk:$(cat ../release.version) . - - - run: - name: Build fn-java-fdk-jdk9 Docker runtime image - command: | - cd runtime - docker build -f Dockerfile-jdk9 -t fnproject/fn-java-fdk:jdk9-$(cat ../release.version) . - - - run: - name: Install fn binary (as it is needed for the integration tests) - command: ./.circleci/install-fn.sh - - - run: - name: Run integration tests - command: REPOSITORY_LOCATION="$STAGING_DIR" FN_JAVA_FDK_VERSION=$(cat release.version) ./integration-tests/run-local.sh - timeout: 1200 - - - run: - name: Copy integration test results to test artifact dir - command: | - mkdir -p "$TEST_ARTIFACT_DIR" - for test_dir in ./integration-tests/main/test-*; do - test_name="$(basename "$test_dir")" - mkdir "${TEST_ARTIFACT_DIR}/${test_name}" - cp -a $(find "$test_dir" -maxdepth 1 -type f) "${TEST_ARTIFACT_DIR}/${test_name}" - done - find "${TEST_ARTIFACT_DIR}" - when: always - - store_artifacts: - name: Upload integration test results to artifacts - path: /tmp/artifacts/tests - - deploy: - name: Release new version - command: | - if [[ "${CIRCLE_BRANCH}" == "master" && -z "${CIRCLE_PR_REPONAME}" ]]; then - git config --global user.email "ci@fnproject.com" - git config --global user.name "CI" - git branch --set-upstream-to=origin/${CIRCLE_BRANCH} ${CIRCLE_BRANCH} - ./.circleci/release.sh - fi - diff --git a/.circleci/install-docker.sh b/.circleci/install-docker.sh deleted file mode 100755 index 688d53a1..00000000 --- a/.circleci/install-docker.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -docker version || true -sudo service docker stop || true -curl -fsSL https://get.docker.com/ | sudo sh -docker version diff --git a/.circleci/install-fn.sh b/.circleci/install-fn.sh deleted file mode 100755 index 615a4ff7..00000000 --- a/.circleci/install-fn.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -set -ex - -source "$(dirname $0)/lib.sh" - -check_command_exists jq -check_command_exists git - -: "${FN_CLI_BINARY:=fn_linux}" -: "${INSTALL_DIR:=/usr/local/bin}" - -FN_BINARY_LOCATION="$(\ - curl -s https://api.github.com/repos/fnproject/cli/releases/latest \ - | jq -r ".assets[] \ - | select(.name | test(\"${FN_CLI_BINARY}\")) \ - | .browser_download_url"\ -)" - -echo "Download fn from $FN_BINARY_LOCATION" -# --location = follow redirects -curl -f --location "$FN_BINARY_LOCATION" --output fn -chmod +x fn -./fn || true # show fn version -sudo cp fn "${INSTALL_DIR}/fn" diff --git a/.circleci/install-go.sh b/.circleci/install-go.sh deleted file mode 100755 index b61512dc..00000000 --- a/.circleci/install-go.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash - -: ${GOVERSION:=1.8.3} -: ${OS:=linux} -: ${ARCH:=amd64} - -set -ex - -go version -go env GOROOT - -# Remove previous Go version -sudo rm -rf /usr/local/go - -# Install Go -BUILD_DIR="/tmp/go-${GOVERSION}.${OS}.${ARCH}" -mkdir -p "$BUILD_DIR" -pushd "$BUILD_DIR" - wget https://storage.googleapis.com/golang/go${GOVERSION}.${OS}-${ARCH}.tar.gz - sudo tar -C /usr/local -xzf go${GOVERSION}.${OS}-${ARCH}.tar.gz - go get -u github.com/golang/dep/... -popd - -mkdir -p "${GOPATH}/bin" - -# Install Glide -if ! type glide 2>&1 > /dev/null; then - curl https://glide.sh/get | sh -fi - -go version -go env GOROOT diff --git a/.circleci/lib.sh b/.circleci/lib.sh deleted file mode 100644 index d6c3e1fe..00000000 --- a/.circleci/lib.sh +++ /dev/null @@ -1,6 +0,0 @@ -check_command_exists() { - local command="$1"; shift - if ! command -v "$command" 2>&1 > /dev/null; then - echo "'$command' is not installed, please install it" >&2 - fi -} diff --git a/.circleci/release.sh b/.circleci/release.sh deleted file mode 100755 index f4a3983e..00000000 --- a/.circleci/release.sh +++ /dev/null @@ -1,83 +0,0 @@ -#!/bin/bash - -set -e -USER=fnproject - -SERVICE=fn-java-fdk -RUNTIME_IMAGE=${SERVICE} -BUILD_IMAGE=${SERVICE}-build - -release_version=$(cat release.version) -if [[ $release_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then - echo "Deploying version $release_version" -else - echo Invalid version $release_version - exit 1 -fi - - -# Calculate new version -version_parts=(${release_version//./ }) -new_minor=$((${version_parts[2]}+1)) -new_version="${version_parts[0]}.${version_parts[1]}.$new_minor" - -if [[ $new_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then - echo "Next version $new_version" -else - echo Invalid new version $new_version - exit 1 -fi - - -# Deploy to bintray -mvn -s ./settings-deploy.xml \ - -DskipTests \ - -DaltDeploymentRepository="fnproject-release-repo::default::$MVN_RELEASE_REPO" \ - -Dfnproject-release-repo.username="$MVN_RELEASE_USER" \ - -Dfnproject-release-repo.password="$MVN_RELEASE_PASSWORD" \ - -DdeployAtEnd=true \ - clean deploy - - -# Regenerate runtime and build images and push them -( - moving_version=${release_version%.*}-latest - - ## jdk8 runtime - docker tag $USER/$RUNTIME_IMAGE:${release_version} $USER/$RUNTIME_IMAGE:latest - docker tag $USER/$RUNTIME_IMAGE:${release_version} $USER/$RUNTIME_IMAGE:${moving_version} - docker push $USER/$RUNTIME_IMAGE:latest - docker push $USER/$RUNTIME_IMAGE:${release_version} - docker push $USER/$RUNTIME_IMAGE:${moving_version} - - ## jdk8 build - docker tag $USER/$BUILD_IMAGE:${release_version} $USER/$BUILD_IMAGE:latest - docker tag $USER/$BUILD_IMAGE:${release_version} $USER/$BUILD_IMAGE:${moving_version} - docker push $USER/$BUILD_IMAGE:latest - docker push $USER/$BUILD_IMAGE:${release_version} - docker push $USER/$BUILD_IMAGE:${moving_version} - - ## jdk9 runtime - docker tag $USER/$RUNTIME_IMAGE:jdk9-${release_version} $USER/$RUNTIME_IMAGE:jdk9-latest - docker tag $USER/$RUNTIME_IMAGE:jdk9-${release_version} $USER/$RUNTIME_IMAGE:jdk9-${moving_version} - docker push $USER/$RUNTIME_IMAGE:jdk9-latest - docker push $USER/$RUNTIME_IMAGE:jdk9-${release_version} - docker push $USER/$RUNTIME_IMAGE:jdk9-${moving_version} - - ## jdk9 build - docker tag $USER/$BUILD_IMAGE:jdk9-${release_version} $USER/$BUILD_IMAGE:jdk9-latest - docker tag $USER/$BUILD_IMAGE:jdk9-${release_version} $USER/$BUILD_IMAGE:jdk9-${moving_version} - docker push $USER/$BUILD_IMAGE:jdk9-latest - docker push $USER/$BUILD_IMAGE:jdk9-${release_version} - docker push $USER/$BUILD_IMAGE:jdk9-${moving_version} -) - - -# Push result to git - -echo $new_version > release.version -git tag -a "$release_version" -m "version $release_version" -git add release.version -git commit -m "$SERVICE: post-$release_version version bump [skip ci]" -git push -git push origin "$release_version" diff --git a/.circleci/update-versions.sh b/.circleci/update-versions.sh deleted file mode 100755 index 1490bd2e..00000000 --- a/.circleci/update-versions.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -release_version=$(cat release.version) -if [[ $release_version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] ; then - echo "Deploying version $release_version" -else - echo Invalid version $release_version - exit 1 -fi - -mvn versions:set -D newVersion=${release_version} versions:update-child-modules - - -# We need to replace the example dependency versions also -# (sed syntax for portability between MacOS and gnu) -find . -name pom.xml | - xargs -n 1 sed -i.bak -e "s|.*|${release_version}|" -find . -name pom.xml.bak -delete - diff --git a/.gitignore b/.gitignore index f93517a1..cb830517 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,18 @@ .DS_Store */dependency-reduced-pom.xml target/ +rpm-package/ *.log \#* .\#* logs/ /headrevtag.txt *.bak -*.versionsBackup \ No newline at end of file +*.versionsBackup +.gradle +examples/gradle-build/build +**/*.classpath +**/*.project +**/*.settings +.oca/ +.csis \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fce1768..619755d2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,4 +16,9 @@ We welcome all contributions! * Don't make breaking changes to the public APIs. * Write tests - especially for public APIs. * Make sure that changes to `api` are backwards compatible with `runtime` and vice-versa. + +## Note for mac users + #### If you run into build failures, try the steps below: + * Install `cmake`, if you don't have it already by running `brew install cmake` + * Go to `fdk-java/runtime/src/main/c` folder and run `./buildit.sh` diff --git a/LICENSE b/LICENSE index 97ee2c36..151b7eab 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017 Oracle Corporation + Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 00000000..4b371e6c --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1,23 @@ + Fn Project + ============ + +Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +========================================================================== +Third Party Dependencies +========================================================================== + +This project includes or depends on code from third party projects. +Attributions are contained in THIRD_PARTY_LICENSES.txt \ No newline at end of file diff --git a/README.md b/README.md index 81eb9874..25d2776b 100644 --- a/README.md +++ b/README.md @@ -1,331 +1,29 @@ -# Fn Java Functions Developer Kit (FDK) [![CircleCI](https://circleci.com/gh/fnproject/fdk-java.svg?style=svg&circle-token=348bec5610c34421f6c436ab8f6a18e153cb1c01)](https://circleci.com/gh/fnproject/fdk-java) -This project adds support for writing functions in Java on the [Fn -platform](https://github.com/fnproject/fn), with full support for Java 9 -as the default out of the box. +# Function Development Kit for Java (FDK for Java) -# FAQ -Some common questions are answered in [our FAQ](docs/FAQ.md). +The Function Development Kit for Java makes it easy to build and deploy Java functions to Fn with full support for Java 11+ as the default out of the box. -# Quick Start Tutorial +Some of the FDK for Java features include: -By following this step-by-step guide you will learn to create, run and deploy -a simple app written in Java on Fn. +- Parsing input and writing output +- Flexible data binding to Java objects +- Function testing using JUnit rules +- And more! -## Pre-requisites +## Learn about the Fn Project -Before you get started you will need the following things: +New to Fn Project? If you want to learn more about using the Fn Project to power your next project, start with the [official documentation](https://github.com/fnproject/docs). -* The [Fn CLI](https://github.com/fnproject/cli) tool -* [Docker-ce 17.06+ installed locally](https://docs.docker.com/engine/installation/) +## Using the Function Development Kit for Java -### Install the Fn CLI tool +For detailed instructions on using the FDK to build and deploy Java functions to Fn, please see the official FDK developer guide in our docs repo here: https://github.com/fnproject/docs/blob/master/fdks/fdk-java/README.md. -To install the Fn CLI tool, just run the following: +## Integrating with OCI Services -``` -curl -LSs https://raw.githubusercontent.com/fnproject/cli/master/install | sh -``` +Use the fn-events library to easily integrate your Function with OCI services; Connector Hub, API Gateway and Notifications. Start with the [README.md](fn-events/README.md) -This will download a shell script and execute it. If the script asks for -a password, that is because it invokes sudo. - -## Your first Function - -### 1. Create your first Java Function: - -```bash -$ mkdir hello-java-function && cd hello-java-function -$ fn init --runtime=java --name your_dockerhub_account/hello -Runtime: java -function boilerplate generated. -func.yaml created -``` - -This creates the boilerplate for a new Java Function based on Maven and Oracle -Java 9. The `pom.xml` includes a dependency on the latest version of the Fn -Java FDK that is useful for developing your Java functions. - -You can now import this project into your favourite IDE as normal. - -### 2. Deep dive into your first Java Function: -We'll now take a look at what makes up our new Java Function. First, lets take -a look at the `func.yaml`: - -```bash -$ cat func.yaml -name: your_dockerhub_account/hello -version: 0.0.1 -runtime: java -cmd: com.example.fn.HelloFunction::handleRequest -``` - -The `cmd` field determines which method is called when your funciton is -invoked. In the generated Function, the `func.yaml` references -`com.example.fn.HelloFunction::handleRequest`. Your functions will likely live -in different classes, and this field should always point to the method to -execute, with the following syntax: - -```text -cmd: :: -``` - -For more information about the fields in `func.yaml`, refer to the [Fn platform -documentation](https://github.com/fnproject/fn/blob/master/docs/function-file.md) -about it. - -Let's also have a brief look at the source: -`src/main/java/com/example/fn/HelloFunction.java`: - -```java -package com.example.fn; - -public class HelloFunction { - - public String handleRequest(String input) { - String name = (input == null || input.isEmpty()) ? "world" : input; - - return "Hello, " + name + "!"; - } - -} -``` - -The function takes some optional input and returns a greeting dependent on it. - -### 3. Run your first Java Function: -You are now ready to run your Function locally using the Fn CLI tool. - -```bash -$ fn build -Building image your_dockerhub_account/hello:0.0.1 -Sending build context to Docker daemon 14.34kB -Step 1/11 : FROM fnproject/fn-java-fdk-build:jdk9-latest as build-stage - ---> 5435658a63ac -Step 2/11 : WORKDIR /function - ---> 37340c5aa451 - -... - -Step 5/11 : RUN mvn package dependency:copy-dependencies -DincludeScope=runtime -DskipTests=true -Dmdep.prependGroupId=true -DoutputDirectory=target --fail-never ----> Running in 58b3b1397ba2 -[INFO] Scanning for projects... -Downloading: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-compiler-plugin/3.3/maven-compiler-plugin-3.3.pom -Downloaded: https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-compiler-plugin/3.3/maven-compiler-plugin-3.3.pom (11 kB at 21 kB/s) - -... - -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 2.228 s -[INFO] Finished at: 2017-06-27T12:06:59Z -[INFO] Final Memory: 18M/143M -[INFO] ------------------------------------------------------------------------ - -... - -Function your_dockerhub_account/hello:0.0.1 built successfully. - -$ fn run -Hello, world! -``` - -The next time you run this, it will execute much quicker as your dependencies -are cached. Try passing in some input this time: - -```bash -$ echo -n "Universe" | fn run -... -Hello, Universe! -``` - -### 4. Testing your function -The Fn Java FDK includes a testing library providing useful [JUnit -4](http://junit.org/junit4/) rules to test functions. Look at the test in -`src/test/java/com/example/fn/HelloFunctionTest.java`: - -```java -package com.example.fn; - -import com.fnproject.fn.testing.*; -import org.junit.*; - -import static org.junit.Assert.*; - -public class HelloFunctionTest { - - @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); - - @Test - public void shouldReturnGreeting() { - testing.givenEvent().enqueue(); - testing.thenRun(HelloFunction.class, "handleRequest"); - - FnResult result = testing.getOnlyResult(); - assertEquals("Hello, world!", result.getBodyAsString()); - } - -} -``` - -This test is very simple: it just enqueues an event with empty input and then -runs the function, checking its output. Under the hood, the `FnTestingRule` is -actually instantiating the same runtime wrapping function invocations, so that -during the test your function will be invoked in exactly the same way that it -would when deployed. - -There is much more functionality to construct tests in the testing library. -Testing functions is covered in more detail in [Testing -Functions](docs/TestingFunctions.md). - -### 5. Run using HTTP and the local Fn server -The previous example used `fn run` to run a function directly via docker, you -can also use the Fn server locally to test the deployment of your function and -the HTTP calls to your functions. - -Open another terminal and start the Fn server: - -```bash -$ fn start -``` - -Then in your original terminal create an app: - -```bash -$ fn apps create java-app -Successfully created app: java-app -``` - -Now deploy your Function using the `fn deploy` command. This will bump the -function's version up, rebuild it, and push the image to the Docker registry, -ready to be used in the function deployment. Finally it will create a route on -the local Fn server, corresponding to your function. - -We are using the `--local` flag to tell fn to skip pushing the image anywhere -as we are just going to run this on our local fn server that we started with -`fn start` above. - -```bash -$ fn deploy --app java-app --local -... -Bumped to version 0.0.2 -Building image hello:0.0.2 -Sending build context to Docker daemon 14.34kB - -... - -Successfully built bf2b7fa55520 -Successfully tagged your_dockerhub_account/hello:0.0.2 -Updating route /hello-java-function using image your_dockerhub_account/hello:0.0.2... -``` - -Call the Function via the Fn CLI: - -```bash -$ fn call java-app /hello-java-function -Hello, world! -``` - -You can also call the Function via curl: - -```bash -$ curl http://localhost:8080/r/java-app/hello-java-function -Hello, world! -``` - -### 6. Something more interesting -The Fn Java FDK supports [flexible data binding](docs/DataBinding.md) to make -it easier for you to map function input and output data to Java objects. - -Below is an example to of a Function that returns a POJO which will be -serialized to JSON using Jackson: - -```java -package com.example.fn; - -public class PojoFunction { - - public static class Greeting { - public final String name; - public final String salutation; - - public Greeting(String salutation, String name) { - this.salutation = salutation; - this.name = name; - } - } - - public Greeting greet(String name) { - if (name == null || name.isEmpty()) - name = "World"; - - return new Greeting("Hello", name); - } - -} -``` - -Update your `func.yaml` to reference the new method: - -```yaml -cmd: com.example.fn.PojoFunction::greet -``` - -Now run your new function: - -```bash -$ fn run -... -{"name":"World","salutation":"Hello"} - -$ echo -n Michael | fn run -... -{"name":"Michael","salutation":"Hello"} -``` - -## 7. Where do I go from here? - -Learn more about the Fn Java FDK by reading the next tutorials in the series. -Also check out the examples in the [`examples` directory](examples) for some -functions demonstrating different features of the Fn Java FDK. - -### Configuring your function - -If you want to set up the state of your function object before the function is -invoked, and to use external configuration variables that you can set up with -the Fn tool, have a look at the [Function -Configuration](docs/FunctionConfiguration.md) tutorial. - -### Input and output bindings - -You have the option of taking more control of how serialization and -deserialization is performed by defining your own bindings. - -See the [Data Binding](docs/DataBinding.md) tutorial for other out-of-the-box -options and the [Extending Data Binding](docs/ExtendingDataBinding.md) tutorial -for how to define and use your own bindings. - -### Asynchronous workflows - -Suppose you want to call out to some other function from yours - perhaps -a function written in a different language, or even one maintained by -a different team. Maybe you then want to do some processing on the result. Or -even have your function interact asynchronously with a completely different -system. Perhaps you also need to maintain some state for the duration of your -function, but you don't want to pay for execution time while you're waiting for -someone else to do their work. - -If this sounds like you, then have a look at the [Fn Flow -quickstart](docs/FnFlowsUserGuide.md). - -# Get help - - * Come over and chat to us on the [fnproject Slack](https://join.slack.com/t/fnproject/shared_invite/enQtMjIwNzc5MTE4ODg3LTdlYjE2YzU1MjAxODNhNGUzOGNhMmU2OTNhZmEwOTcxZDQxNGJiZmFiMzNiMTk0NjU2NTIxZGEyNjI0YmY4NTA). - * Raise an issue in [our github](https://github.com/fnproject/fn-java-fdk/). - -# Contributing +## Contributing to the Function Development Kit for Java Please see [CONTRIBUTING.md](CONTRIBUTING.md). + diff --git a/THIRD_PARTY_LICENSES.txt b/THIRD_PARTY_LICENSES.txt new file mode 100644 index 00000000..12003c06 --- /dev/null +++ b/THIRD_PARTY_LICENSES.txt @@ -0,0 +1,999 @@ +Third Party Attributions + +The following software (or subsets of the software) are dependencies +of this product. They are identified by the Fn Project Java FDK module(s) that use +them. + +The first section ("Third Party Runtime Dependencies") contains dependencies +that might be used at runtime by an Fn Project Java FDK application. + +The second section ("Third Party Attributions for Examples, Tests, Builds, etc") +contains dependencies that are used in examples and to test and build Fn Project Java FDK. +They are likely not needed at runtime by an Fn Project Java FDK application. + +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +Third Party Runtime Dependencies +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +oci-java-sdk is dual-licensed to you under the Universal Permissive License (UPL) 1.0 or Apache License 2.0. See below for license terms. You may choose either license. +Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved. +License: Copies of the Apache License V2 and the Universal Permissive License 1.0 are included at the end of this file. + +-------------------------------------------------------------------------------- + +fdk-java is licensed under the Apache License, Version 2.0. +License: A copy of the Apache License V2 is included at the end of this file. + +-------------------------------------------------------------------------------- + +Apache Commons Logging +Copyright 2003-2016 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +License: Apache License 2.0, a copy of the license is included at the end of this file. + +-------------------------------------------------------------------------------- + +Apache Commons IO +Copyright 2002-2017 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). +License: Apache License 2.0, a copy of the license is included at the end of this file. + +-------------------------------------------------------------------------------- + +Apache HttpComponents Core +Copyright 2005-2020 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +License: Apache License 2.0, a copy of the license is included at the end of this file. + +-------------------------------------------------------------------------------- + +Apache HttpComponents Client (including HttpMime) +Copyright 1999-2018 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +License: Apache License 2.0, a copy of the license is included at the end of this file. + +-------------------------------------------------------------------------------- + +Jackson Databind +Copyright (c) 2019 Tatu Saloranta + +License: Apache License 2.0, a copy of the license is included at the end of this file. + +NOTICE FILE: +=============== +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers, as well as supported +commercially by FasterXML.com. + +## Licensing + +Jackson core and extension components may be licensed under different licenses. +To find the details that apply to this artifact see the accompanying LICENSE file. +For more information, including possible other licensing options, contact +FasterXML.com (http://fasterxml.com). + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. + +-------------------------------------------------------------------------------- + +Typetools +Copyright 2010-2019 Jonathan Halterman and friends. Released under the Apache 2.0 license. +License: Apache License 2.0, a copy of the license is included at the end of this file. + +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +Third Party Attributions for Examples, Tests, Builds, etc + +The following software (or subsets of the software) is used when building the +Fn Project Java FDK, or in the examples and tests. They are generally not required by +users of the Fn Project Java FDK and not required during runtime. +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= + +Mockito Core +Copyright (c) 2007 Mockito contributors +The MIT License + +License: The MIT License, a copy of the license is included at the end of this file. + +................................................................................ + +Fourth Party Dependencies + +Byte Buddy (without dependencies) +Copyright 2014 - 2020 Rafael Winterhalter +Apache License Version 2.0 + + +Byte Buddy Java agent +Copyright 2014 - 2020 Rafael Winterhalter +Apache License Version 2.0 + +Objenesis +Copyright 2006-2020 the original author or authors. +Apache License Version 2.0 + +License: Apache License 2.0, a copy of the license is included at the end of this file. + +-------------------------------------------------------------------------------- + +AssertJ Core + +Copyright 2012-2020 the original author or authors. +Apache License Version 2.0 + +License: Apache License 2.0, a copy of the license is included at the end of this file. + +................................................................................ + +Fourth Party Dependencies + +Byte Buddy (without dependencies) +Copyright 2014 - 2020 Rafael Winterhalter +Apache License Version 2.0 + +Byte Buddy Java agent +Copyright 2014 - 2020 Rafael Winterhalter +Apache License Version 2.0 + +opentest4j +Copyright 2015-2018 the original author or authors. +Apache License Version 2.0 + +License: Apache License 2.0, a copy of the license is included at the end of this file. + + +Hamcrest +Copyright (c) 2000-2015 www.hamcrest.org +BSD License + +License: BSD License, a copy of the license is included at the end of this file. + + +JUnit 4 +Copyright (c) JUnit. All Rights Reserved. +Eclipse Public License - v 1.0 +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE +TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR +DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS +AGREEMENT. + +License: Eclipse Public License - v 1.0, a copy of the license is included at the end of this file. + + +JUnit Jupiter API +Copyright 2015-2020 the original author or authors. +Eclipse Public License - v 2.0 + +License: Eclipse Public License - v 2.0, a copy of the license is included at the end of this file. + +-------------------------------------------------------------------------------- + +JUnit 4 +Copyright (c) JUnit. All Rights Reserved. +Eclipse Public License - v 1.0 +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE +TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR +DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS +AGREEMENT. + +License: Eclipse Public License - v 1.0, a copy of the license is included at the end of this file. + +................................................................................ + +Fourth Party Dependencies + +Hamcrest +Copyright (c) 2000-2015 www.hamcrest.org +BSD License + +License: BSD License, a copy of the license is included at the end of this file. + +================================================================================ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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 + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +================================================================================ +The Universal Permissive License (UPL), Version 1.0 + +Subject to the condition set forth below, permission is hereby granted to any +person obtaining a copy of this software, associated documentation and/or data +(collectively the "Software"), free of charge and under any and all copyright +rights in the Software, and any and all patent rights owned or freely +licensable by each licensor hereunder covering either (i) the unmodified +Software as contributed to or provided by such licensor, or (ii) the Larger +Works (as defined below), to deal in both + +(a) the Software, and +(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if +one is included with the Software (each a "Larger Work" to which the Software +is contributed by such licensors), + +without restriction, including without limitation the rights to copy, create +derivative works of, display, perform, and distribute the Software and make, +use, sell, offer for sale, import, export, have made, and have sold the +Software and the Larger Work(s), and to sublicense the foregoing rights on +either these or other terms. + +This license is subject to the following condition: +The above copyright notice and either this complete permission notice or at +a minimum a reference to the UPL must be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +================================================================================ +The MIT License + +Copyright (c) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +================================================================================ +BSD License + +Copyright (c) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of +conditions and the following disclaimer. Redistributions in binary form must reproduce +the above copyright notice, this list of conditions and the following disclaimer in +the documentation and/or other materials provided with the distribution. + +Neither the name of Hamcrest nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT +SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY +WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +================================================================================ +Common Public License Version 1.0 (CPL) +(NOTE: This license has been superseded by the Eclipse Public License) + +Eclipse Public License - v 1.0 + +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS COMMON PUBLIC +LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM +CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + +a) in the case of the initial Contributor, the initial code and documentation +distributed under this Agreement, and + +b) in the case of each subsequent Contributor: + +i) changes to the Program, and + +ii) additions to the Program; + +where such changes and/or additions to the Program originate from and are +distributed by that particular Contributor. A Contribution 'originates' from +a Contributor if it was added to the Program by such Contributor itself or +anyone acting on such Contributor's behalf. Contributions do not include +additions to the Program which: (i) are separate modules of software +distributed in conjunction with the Program under their own license agreement, +and (ii) are not derivative works of the Program. + +"Contributor" means any person or entity that distributes the Program. + +"Licensed Patents " mean patent claims licensable by a Contributor which are +necessarily infringed by the use or sale of its Contribution alone or when +combined with the Program. + +"Program" means the Contributions distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement, +including all Contributors. + +2. GRANT OF RIGHTS + +a) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free copyright license to +reproduce, prepare derivative works of, publicly display, publicly perform, +distribute and sublicense the Contribution of such Contributor, if any, and +such derivative works, in source code and object code form. + +b) Subject to the terms of this Agreement, each Contributor hereby grants +Recipient a non-exclusive, worldwide, royalty-free patent license under +Licensed Patents to make, use, sell, offer to sell, import and otherwise +transfer the Contribution of such Contributor, if any, in source code and +object code form. This patent license shall apply to the combination of the +Contribution and the Program if, at the time the Contribution is added by the +Contributor, such addition of the Contribution causes such combination to be +covered by the Licensed Patents. The patent license shall not apply to any +other combinations which include the Contribution. No hardware per se is +licensed hereunder. + +c) Recipient understands that although each Contributor grants the licenses to +its Contributions set forth herein, no assurances are provided by any +Contributor that the Program does not infringe the patent or other intellectual +property rights of any other entity. Each Contributor disclaims any liability +to Recipient for claims brought by any other entity based on infringement of +intellectual property rights or otherwise. As a condition to exercising the +rights and licenses granted hereunder, each Recipient hereby assumes sole +responsibility to secure any other intellectual property rights needed, if any. +For example, if a third party patent license is required to allow Recipient to +distribute the Program, it is Recipient's responsibility to acquire that +license before distributing the Program. + +d) Each Contributor represents that to its knowledge it has sufficient +copyright rights in its Contribution, if any, to grant the copyright license +set forth in this Agreement. + +3. REQUIREMENTS + +A Contributor may choose to distribute the Program in object code form under +its own license agreement, provided that: + +a) it complies with the terms and conditions of this Agreement; and + +b) its license agreement: + +i) effectively disclaims on behalf of all Contributors all warranties and +conditions, express and implied, including warranties or conditions of title +and non-infringement, and implied warranties or conditions of merchantability +and fitness for a particular purpose; + +ii) effectively excludes on behalf of all Contributors all liability for +damages, including direct, indirect, special, incidental and consequential +damages, such as lost profits; + +iii) states that any provisions which differ from this Agreement are offered by +that Contributor alone and not by any other party; and + +iv) states that source code for the Program is available from such Contributor, +and informs licensees how to obtain it in a reasonable manner on or through a +medium customarily used for software exchange. + +When the Program is made available in source code form: + +a) it must be made available under this Agreement; and + +b) a copy of this Agreement must be included with each copy of the Program. + +Contributors may not remove or alter any copyright notices contained within the +Program. + +Each Contributor must identify itself as the originator of its Contribution, if +any, in a manner that reasonably allows subsequent Recipients to identify the +originator of the Contribution. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities with +respect to end users, business partners and the like. While this license is +intended to facilitate the commercial use of the Program, the Contributor who +includes the Program in a commercial product offering should do so in a manner +which does not create potential liability for other Contributors. Therefore, if +a Contributor includes the Program in a commercial product offering, such +Contributor ("Commercial Contributor") hereby agrees to defend and indemnify +every other Contributor ("Indemnified Contributor") against any losses, damages +and costs (collectively "Losses") arising from claims, lawsuits and other legal +actions brought by a third party against the Indemnified Contributor to the +extent caused by the acts or omissions of such Commercial Contributor in +connection with its distribution of the Program in a commercial product +offering. The obligations in this section do not apply to any claims or Losses +relating to any actual or alleged intellectual property infringement. In order +to qualify, an Indemnified Contributor must: a) promptly notify the Commercial +Contributor in writing of such claim, and b) allow the Commercial Contributor +to control, and cooperate with the Commercial Contributor in, the defense and +any related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product +offering, Product X. That Contributor is then a Commercial Contributor. If that +Commercial Contributor then makes performance claims, or offers warranties +related to Product X, those performance claims and warranties are such +Commercial Contributor's responsibility alone. Under this section, the +Commercial Contributor would have to defend claims against the other +Contributors related to those performance claims and warranties, and if a court +requires any other Contributor to pay any damages as a result, the Commercial +Contributor must pay those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, +NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each +Recipient is solely responsible for determining the appropriateness of using +and distributing the Program and assumes all risks associated with its +exercise of rights under this Agreement, including but not limited to the +risks and costs of program errors, compliance with applicable laws, damage to +or loss of data, programs or equipment, and unavailability or interruption of +operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY +CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION +LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING +IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY +RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under applicable +law, it shall not affect the validity or enforceability of the remainder of the +terms of this Agreement, and without further action by the parties hereto, such +provision shall be reformed to the minimum extent necessary to make such +provision valid and enforceable. + +If Recipient institutes patent litigation against a Contributor with respect to +a patent applicable to software (including a cross-claim or counterclaim in a +lawsuit), then any patent licenses granted by that Contributor to such +Recipient under this Agreement shall terminate as of the date such litigation +is filed. In addition, if Recipient institutes patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software or +hardware) infringes such Recipient's patent(s), then such Recipient's rights +granted under Section 2(b) shall terminate as of the date such litigation is +filed. + +All Recipient's rights under this Agreement shall terminate if it fails to +comply with any of the material terms or conditions of this Agreement and does +not cure such failure in a reasonable period of time after becoming aware of +such noncompliance. If all Recipient's rights under this Agreement terminate, +Recipient agrees to cease use and distribution of the Program as soon as +reasonably practicable. However, Recipient's obligations under this Agreement +and any licenses granted by Recipient relating to the Program shall continue +and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in +order to avoid inconsistency the Agreement is copyrighted and may only be +modified in the following manner. The Agreement Steward reserves the right to +publish new versions (including revisions) of this Agreement from time to time. +No one other than the Agreement Steward has the right to modify this Agreement. +IBM is the initial Agreement Steward. IBM may assign the responsibility to +serve as the Agreement Steward to a suitable separate entity. Each new version +of the Agreement will be given a distinguishing version number. The Program +(including Contributions) may always be distributed subject to the version of +the Agreement under which it was received. In addition, after a new version of +the Agreement is published, Contributor may elect to distribute the Program +(including its Contributions) under the new version. Except as expressly stated +in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to +the intellectual property of any Contributor under this Agreement, whether +expressly, by implication, estoppel or otherwise. All rights in the Program not +expressly granted under this Agreement are reserved. + +This Agreement is governed by the laws of the State of New York and the +intellectual property laws of the United States of America. No party to this +Agreement will bring a legal action under this Agreement more than one year +after the cause of action arose. Each party waives its rights to a jury trial +in any resulting litigation. + +================================================================================ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/api/pom.xml b/api/pom.xml index 71190839..eef37b0c 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -1,4 +1,22 @@ + + @@ -9,18 +27,26 @@ 4.0.0 - - UTF-8 - - + api api + + + junit + junit + test + + + org.assertj + assertj-core + test + + org.apache.maven.plugins maven-javadoc-plugin - 3.0.0-M1 attach-javadocs @@ -30,6 +56,22 @@ + + org.netbeans.tools + sigtest-maven-plugin + + + + check + + + + + src/main/api/snapshot.sigfile + strictcheck + com.fnproject.fn.api,com.fnproject.fn.api.exception + + diff --git a/api/src/main/api/snapshot.sigfile b/api/src/main/api/snapshot.sigfile new file mode 100644 index 00000000..096b2dee --- /dev/null +++ b/api/src/main/api/snapshot.sigfile @@ -0,0 +1,320 @@ +#Signature file v4.1 +#Version 1.0.0-SNAPSHOT + +CLSS public abstract interface !annotation com.fnproject.fn.api.FnConfiguration + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[METHOD]) +intf java.lang.annotation.Annotation + +CLSS public abstract interface !annotation com.fnproject.fn.api.FnFeature + anno 0 java.lang.annotation.Repeatable(java.lang.Class value=class com.fnproject.fn.api.FnFeatures) + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.Class value() + +CLSS public abstract interface !annotation com.fnproject.fn.api.FnFeatures + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation +meth public abstract com.fnproject.fn.api.FnFeature[] value() + +CLSS public abstract interface com.fnproject.fn.api.FunctionInvoker +innr public final static !enum Phase +meth public abstract java.util.Optional tryInvoke(com.fnproject.fn.api.InvocationContext,com.fnproject.fn.api.InputEvent) + +CLSS public final static !enum com.fnproject.fn.api.FunctionInvoker$Phase + outer com.fnproject.fn.api.FunctionInvoker +fld public final static com.fnproject.fn.api.FunctionInvoker$Phase Call +fld public final static com.fnproject.fn.api.FunctionInvoker$Phase PreCall +meth public static com.fnproject.fn.api.FunctionInvoker$Phase valueOf(java.lang.String) +meth public static com.fnproject.fn.api.FunctionInvoker$Phase[] values() +supr java.lang.Enum + +CLSS public final com.fnproject.fn.api.Headers +intf java.io.Serializable +meth public java.util.Map getAll() +meth public !varargs com.fnproject.fn.api.Headers addHeader(java.lang.String,java.lang.String,java.lang.String[]) +meth public !varargs com.fnproject.fn.api.Headers setHeader(java.lang.String,java.lang.String,java.lang.String[]) +meth public boolean equals(java.lang.Object) +meth public com.fnproject.fn.api.Headers removeHeader(java.lang.String) +meth public com.fnproject.fn.api.Headers setHeader(java.lang.String,java.util.Collection) +meth public com.fnproject.fn.api.Headers setHeaders(java.util.Map>) +meth public int hashCode() +meth public java.lang.String toString() +meth public java.util.Collection keys() +meth public java.util.List getAllValues(java.lang.String) +meth public java.util.Map> asMap() +meth public java.util.Optional get(java.lang.String) +meth public static com.fnproject.fn.api.Headers emptyHeaders() +meth public static com.fnproject.fn.api.Headers fromMap(java.util.Map) +meth public static com.fnproject.fn.api.Headers fromMultiHeaderMap(java.util.Map>) +meth public static java.lang.String canonicalKey(java.lang.String) +supr java.lang.Object +hfds emptyHeaders,headerName,headers + +CLSS public abstract interface !annotation com.fnproject.fn.api.InputBinding + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[PARAMETER]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.Class coercion() + +CLSS public abstract interface com.fnproject.fn.api.InputCoercion<%0 extends java.lang.Object> +meth public abstract java.util.Optional<{com.fnproject.fn.api.InputCoercion%0}> tryCoerceParam(com.fnproject.fn.api.InvocationContext,int,com.fnproject.fn.api.InputEvent,com.fnproject.fn.api.MethodWrapper) + +CLSS public abstract interface com.fnproject.fn.api.InputEvent +intf java.io.Closeable +meth public abstract <%0 extends java.lang.Object> {%%0} consumeBody(java.util.function.Function) +meth public abstract com.fnproject.fn.api.Headers getHeaders() +meth public abstract java.lang.String getCallID() +meth public abstract java.time.Instant getDeadline() + +CLSS public abstract interface com.fnproject.fn.api.InvocationContext +meth public abstract !varargs void setResponseHeader(java.lang.String,java.lang.String,java.lang.String[]) +meth public abstract com.fnproject.fn.api.Headers getRequestHeaders() +meth public abstract com.fnproject.fn.api.RuntimeContext getRuntimeContext() +meth public abstract void addListener(com.fnproject.fn.api.InvocationListener) +meth public abstract void addResponseHeader(java.lang.String,java.lang.String) +meth public void setResponseContentType(java.lang.String) + +CLSS public abstract interface com.fnproject.fn.api.InvocationListener +intf java.util.EventListener +meth public abstract void onFailure() +meth public abstract void onSuccess() + +CLSS public abstract interface com.fnproject.fn.api.MethodWrapper +meth public abstract com.fnproject.fn.api.TypeWrapper getParamType(int) +meth public abstract com.fnproject.fn.api.TypeWrapper getReturnType() +meth public abstract java.lang.Class getTargetClass() +meth public abstract java.lang.reflect.Method getTargetMethod() +meth public int getParameterCount() +meth public java.lang.String getLongName() + +CLSS public abstract interface !annotation com.fnproject.fn.api.OutputBinding + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[METHOD]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.Class coercion() + +CLSS public abstract interface com.fnproject.fn.api.OutputCoercion +meth public abstract java.util.Optional wrapFunctionResult(com.fnproject.fn.api.InvocationContext,com.fnproject.fn.api.MethodWrapper,java.lang.Object) + +CLSS public abstract interface com.fnproject.fn.api.OutputEvent +fld public final static java.lang.String CONTENT_TYPE_HEADER = "Content-Type" +innr public final static !enum Status +meth public abstract com.fnproject.fn.api.Headers getHeaders() +meth public abstract com.fnproject.fn.api.OutputEvent$Status getStatus() +meth public abstract void writeToOutput(java.io.OutputStream) throws java.io.IOException +meth public boolean isSuccess() +meth public com.fnproject.fn.api.OutputEvent withHeaders(com.fnproject.fn.api.Headers) +meth public java.util.Optional getContentType() +meth public static com.fnproject.fn.api.OutputEvent emptyResult(com.fnproject.fn.api.OutputEvent$Status) +meth public static com.fnproject.fn.api.OutputEvent fromBytes(byte[],com.fnproject.fn.api.OutputEvent$Status,java.lang.String) +meth public static com.fnproject.fn.api.OutputEvent fromBytes(byte[],com.fnproject.fn.api.OutputEvent$Status,java.lang.String,com.fnproject.fn.api.Headers) + +CLSS public final static !enum com.fnproject.fn.api.OutputEvent$Status + outer com.fnproject.fn.api.OutputEvent +fld public final static com.fnproject.fn.api.OutputEvent$Status FunctionError +fld public final static com.fnproject.fn.api.OutputEvent$Status FunctionTimeout +fld public final static com.fnproject.fn.api.OutputEvent$Status InternalError +fld public final static com.fnproject.fn.api.OutputEvent$Status Success +meth public int getCode() +meth public static com.fnproject.fn.api.OutputEvent$Status valueOf(java.lang.String) +meth public static com.fnproject.fn.api.OutputEvent$Status[] values() +supr java.lang.Enum +hfds code + +CLSS public abstract interface com.fnproject.fn.api.QueryParameters +meth public abstract java.util.List getValues(java.lang.String) +meth public abstract java.util.Map> getAll() +meth public abstract java.util.Optional get(java.lang.String) + +CLSS public abstract interface com.fnproject.fn.api.RuntimeContext +meth public abstract <%0 extends java.lang.Object> java.util.Optional<{%%0}> getAttribute(java.lang.String,java.lang.Class<{%%0}>) +meth public abstract com.fnproject.fn.api.MethodWrapper getMethod() +meth public abstract java.lang.String getAppID() +meth public abstract java.lang.String getFunctionID() +meth public java.lang.String getAppName() +meth public java.lang.String getFunctionName() +meth public abstract java.util.List getInputCoercions(com.fnproject.fn.api.MethodWrapper,int) +meth public abstract java.util.List getOutputCoercions(java.lang.reflect.Method) +meth public abstract java.util.Map getConfiguration() +meth public abstract java.util.Optional getInvokeInstance() +meth public abstract java.util.Optional getConfigurationByKey(java.lang.String) +meth public abstract void addInputCoercion(com.fnproject.fn.api.InputCoercion) +meth public abstract void addInvoker(com.fnproject.fn.api.FunctionInvoker,com.fnproject.fn.api.FunctionInvoker$Phase) +meth public abstract void addOutputCoercion(com.fnproject.fn.api.OutputCoercion) +meth public abstract void setAttribute(java.lang.String,java.lang.Object) +meth public void setInvoker(com.fnproject.fn.api.FunctionInvoker) + +CLSS public abstract interface com.fnproject.fn.api.RuntimeFeature +meth public abstract void initialize(com.fnproject.fn.api.RuntimeContext) + +CLSS public abstract interface com.fnproject.fn.api.TypeWrapper +meth public abstract java.lang.Class getParameterClass() + +CLSS public com.fnproject.fn.api.exception.FunctionConfigurationException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +supr com.fnproject.fn.api.exception.FunctionLoadException + +CLSS public com.fnproject.fn.api.exception.FunctionInputHandlingException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +supr java.lang.RuntimeException + +CLSS public abstract com.fnproject.fn.api.exception.FunctionLoadException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +supr java.lang.RuntimeException + +CLSS public com.fnproject.fn.api.exception.FunctionOutputHandlingException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Exception) +supr java.lang.RuntimeException + + +CLSS public abstract interface com.fnproject.fn.api.httpgateway.HTTPGatewayContext +meth public abstract InvocationContext getInvocationContext() +meth public abstract Headers getHeaders() +meth public abstract String getRequestURL() +meth public abstract String getMethod() +meth public abstract QueryParameters getQueryParameters() +meth public abstract void addResponseHeader(String key, String value) +meth public abstract void setResponseHeader(String key, String v1, String... vs) +meth public abstract void setStatusCode(int code) + +CLSS public abstract interface com.fnproject.fn.api.tracing.TracingContext +meth public abstract InvocationContext getInvocationContext() +meth public abstract RuntimeContext getRuntimeContext() +meth public abstract java.lang.String getServiceName() +meth public abstract java.lang.String getTraceCollectorURL() +meth public abstract java.lang.String getTraceId() +meth public abstract java.lang.String getSpanId() +meth public abstract java.lang.String getParentSpanId() +meth public abstract java.lang.Boolean isSampled() +meth public abstract java.lang.String getFlags() +meth public abstract java.lang.Boolean isTracingEnabled() +meth public abstract java.lang.String getAppName() +meth public abstract java.lang.String getFunctionName() + +CLSS public abstract interface java.io.Closeable +intf java.lang.AutoCloseable +meth public abstract void close() throws java.io.IOException + +CLSS public abstract interface java.io.Serializable + +CLSS public abstract interface java.lang.AutoCloseable +meth public abstract void close() throws java.lang.Exception + +CLSS public abstract interface java.lang.Comparable<%0 extends java.lang.Object> +meth public abstract int compareTo({java.lang.Comparable%0}) + +CLSS public abstract java.lang.Enum<%0 extends java.lang.Enum<{java.lang.Enum%0}>> +cons protected init(java.lang.String,int) +intf java.io.Serializable +intf java.lang.Comparable<{java.lang.Enum%0}> +meth protected final java.lang.Object clone() throws java.lang.CloneNotSupportedException +meth protected final void finalize() +meth public final boolean equals(java.lang.Object) +meth public final int compareTo({java.lang.Enum%0}) +meth public final int hashCode() +meth public final int ordinal() +meth public final java.lang.Class<{java.lang.Enum%0}> getDeclaringClass() +meth public final java.lang.String name() +meth public java.lang.String toString() +meth public static <%0 extends java.lang.Enum<{%%0}>> {%%0} valueOf(java.lang.Class<{%%0}>,java.lang.String) +supr java.lang.Object +hfds name,ordinal + +CLSS public java.lang.Exception +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.Throwable +hfds serialVersionUID + +CLSS public java.lang.Object +cons public init() +meth protected java.lang.Object clone() throws java.lang.CloneNotSupportedException +meth protected void finalize() throws java.lang.Throwable +meth public boolean equals(java.lang.Object) +meth public final java.lang.Class getClass() +meth public final void notify() +meth public final void notifyAll() +meth public final void wait() throws java.lang.InterruptedException +meth public final void wait(long) throws java.lang.InterruptedException +meth public final void wait(long,int) throws java.lang.InterruptedException +meth public int hashCode() +meth public java.lang.String toString() + +CLSS public java.lang.RuntimeException +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.Exception +hfds serialVersionUID + +CLSS public java.lang.Throwable +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +intf java.io.Serializable +meth public final java.lang.Throwable[] getSuppressed() +meth public final void addSuppressed(java.lang.Throwable) +meth public java.lang.StackTraceElement[] getStackTrace() +meth public java.lang.String getLocalizedMessage() +meth public java.lang.String getMessage() +meth public java.lang.String toString() +meth public java.lang.Throwable fillInStackTrace() +meth public java.lang.Throwable getCause() +meth public java.lang.Throwable initCause(java.lang.Throwable) +meth public void printStackTrace() +meth public void printStackTrace(java.io.PrintStream) +meth public void printStackTrace(java.io.PrintWriter) +meth public void setStackTrace(java.lang.StackTraceElement[]) +supr java.lang.Object +hfds CAUSE_CAPTION,EMPTY_THROWABLE_ARRAY,NULL_CAUSE_MESSAGE,SELF_SUPPRESSION_MESSAGE,SUPPRESSED_CAPTION,SUPPRESSED_SENTINEL,UNASSIGNED_STACK,backtrace,cause,detailMessage,serialVersionUID,stackTrace,suppressedExceptions +hcls PrintStreamOrWriter,SentinelHolder,WrappedPrintStream,WrappedPrintWriter + +CLSS public abstract interface java.lang.annotation.Annotation +meth public abstract boolean equals(java.lang.Object) +meth public abstract int hashCode() +meth public abstract java.lang.Class annotationType() +meth public abstract java.lang.String toString() + +CLSS public abstract interface !annotation java.lang.annotation.Documented + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation + +CLSS public abstract interface !annotation java.lang.annotation.Repeatable + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.Class value() + +CLSS public abstract interface !annotation java.lang.annotation.Retention + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.annotation.RetentionPolicy value() + +CLSS public abstract interface !annotation java.lang.annotation.Target + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.annotation.ElementType[] value() + +CLSS public abstract interface java.util.EventListener + diff --git a/api/src/main/java/com/fnproject/fn/api/FnConfiguration.java b/api/src/main/java/com/fnproject/fn/api/FnConfiguration.java index 62a70a13..52273461 100644 --- a/api/src/main/java/com/fnproject/fn/api/FnConfiguration.java +++ b/api/src/main/java/com/fnproject/fn/api/FnConfiguration.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.lang.annotation.ElementType; diff --git a/api/src/main/java/com/fnproject/fn/api/FnFeature.java b/api/src/main/java/com/fnproject/fn/api/FnFeature.java new file mode 100644 index 00000000..6b9165be --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/FnFeature.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api; + +import java.lang.annotation.*; + +/** + * Annotation to be used in user function classes to enable runtime-wide feature. + * + * Runtime features are initialized at the point that the function class is loaded but prior to the call chain. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Repeatable(FnFeatures.class) +public @interface FnFeature { + /** + * The feature class to load this must have a zero-arg public constructor + * @return feature class + */ + Class value(); +} diff --git a/api/src/main/java/com/fnproject/fn/api/FnFeatures.java b/api/src/main/java/com/fnproject/fn/api/FnFeatures.java new file mode 100644 index 00000000..edb021f0 --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/FnFeatures.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be used in user function classes to enable runtime-wide feature. + * + * Runtime features are initialized at the point that the function class is loaded but prior to the call chain. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface FnFeatures { + FnFeature[] value(); +} diff --git a/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java b/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java index 7f1fa29a..691e6a90 100644 --- a/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java +++ b/api/src/main/java/com/fnproject/fn/api/FunctionInvoker.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.util.Optional; @@ -6,6 +22,22 @@ * Handles the invocation of a given function call */ public interface FunctionInvoker { + /** + * Phase determines a loose ordering for invocation handler processing + * this should be used with {@link RuntimeContext#addInvoker(FunctionInvoker, Phase)} to add new invoke handlers to a runtime + */ + enum Phase { + /** + * The Pre-Call phase runs before the main function call, all {@link FunctionInvoker} handlers added at this phase are tried prior to calling the {@link Phase#Call} phase + * This phase is typically used for handlers that /may/ intercept the request based on request attributes + */ + PreCall, + /** + * The Call Phase indicates invokers that should handle call values - typically a given runtime will only be handled by one of these + */ + Call + } + /** * Optionally handles an invocation chain for this function *

diff --git a/api/src/main/java/com/fnproject/fn/api/Headers.java b/api/src/main/java/com/fnproject/fn/api/Headers.java index 2e069c03..52618d2c 100644 --- a/api/src/main/java/com/fnproject/fn/api/Headers.java +++ b/api/src/main/java/com/fnproject/fn/api/Headers.java @@ -1,16 +1,78 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; +import java.io.Serializable; import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Stream; /** - * Represents the headers on an HTTP request or response. Multiple headers with the same key are collapsed into a single - * entry where the values are concatenated by commas as per the HTTP spec (RFC 7230). + * Represents a set of String-String[] header attributes, per HTTP headers. + *

+ * Internally header keys are always canonicalized using HTTP header conventions + *

+ * Headers objects are immutable + *

+ * Keys are are stored and compared in a case-insensitive way and are canonicalised according to RFC 7230 conventions such that : + * + *

    + *
  • a-header
  • + *
  • A-Header
  • + *
  • A-HeaDer
  • + *
+ * are all equivalent - keys are returned in the canonical form (lower cased except for leading characters) + * Where keys do not comply with HTTP header naming they are left as is. */ -public final class Headers { - private Map headers; +public final class Headers implements Serializable { + private static final Headers emptyHeaders = new Headers(Collections.emptyMap()); + private Map> headers; + + private Headers(Map> headersIn) { + this.headers = headersIn; + } + + private static Pattern headerName = Pattern.compile("[A-Za-z0-9!#%&'*+-.^_`|~]+"); + + public Map getAll() { + return headers; + } + + /** + * Calculates the canonical key (cf RFC 7230) for a header + *

+ * If the header contains invalid characters it returns the original header + * + * @param key the header key to canonicalise + * @return a canonical key or the original key if the input contains invalid character + */ + public static String canonicalKey(String key) { + if (!headerName.matcher(key).matches()) { + return key; + } + String parts[] = key.split("-", -1); + for (int i = 0; i < parts.length; i++) { + String p = parts[i]; + if (p.length() > 0) { + parts[i] = p.substring(0, 1).toUpperCase() + p.substring(1).toLowerCase(); + } + } + return String.join("-", parts); - private Headers(Map headers) { - this.headers = headers; } /** @@ -21,6 +83,23 @@ private Headers(Map headers) { * @return {@code Headers} built from headers map */ public static Headers fromMap(Map headers) { + Objects.requireNonNull(headers, "headersIn"); + Map> h = new HashMap<>(); + headers.forEach((k, v) -> h.put(canonicalKey(k), Collections.singletonList(v))); + return new Headers(Collections.unmodifiableMap(new HashMap<>(h))); + } + + /** + * Build a headers object from a map composed of (name, value) entries, we take a copy of the map and + * disallow any further modification + * + * @param headers underlying collection of header entries to copy + * @return {@code Headers} built from headers map + */ + public static Headers fromMultiHeaderMap(Map> headers) { + Map> hm = new HashMap<>(); + + headers.forEach((k, vs) -> hm.put(canonicalKey(k), new ArrayList<>(vs))); return new Headers(Collections.unmodifiableMap(new HashMap<>(Objects.requireNonNull(headers)))); } @@ -30,43 +109,136 @@ public static Headers fromMap(Map headers) { * @return empty headers */ public static Headers emptyHeaders() { - return new Headers(Collections.emptyMap()); + return emptyHeaders; } /** - * Creates a new headers object with the specified header added + * Sets a map of headers, overwriting any headers in the current headers with the respective values * + * @param vals a map of headers + * @return a new headers object with thos headers set + */ + public Headers setHeaders(Map> vals) { + Objects.requireNonNull(vals, "vals"); + Map> nm = new HashMap<>(headers); + vals.forEach((k, vs) -> { + vs.forEach(v -> Objects.requireNonNull(v, "header list contains null entries")); + nm.put(canonicalKey(k), vs); + }); + return new Headers(nm); + } + + /** + * Creates a new headers object with the specified header added - if a header with the same key existed it the new value is appended + *

* This will overwrite an existing header with an exact name match + * * @param key new header key - * @param value new header value + * @param v1 new header value + * @param vs additional header values to set * @return a new headers object with the specified header added */ - public Headers withHeader(String key, String value){ - Map newHeaders = new HashMap<>(); - newHeaders.putAll(getAll()); - newHeaders.put(key,value); - return new Headers(newHeaders); + public Headers addHeader(String key, String v1, String... vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(key, "value"); + + String canonKey = canonicalKey(key); + + Map> nm = new HashMap<>(headers); + List current = nm.get(canonKey); + + if (current == null) { + List s = new ArrayList<>(); + s.add(v1); + s.addAll(Arrays.asList(vs)); + + nm.put(canonKey, Collections.unmodifiableList(s)); + } else { + List s = new ArrayList<>(current); + s.add(v1); + s.addAll(Arrays.asList(vs)); + nm.put(canonKey, Collections.unmodifiableList(s)); + } + return new Headers(nm); + } /** - * Returns the header matching the specified key. This matches headers in a case-insensitive way and substitutes - * underscore and hyphen characters such that : "CONTENT_TYPE" and "Content-type" are equivalent. If no matching - * header is found then {@code Optional.empty} is returned. + * Creates a new headers object with the specified header set - this overwrites any existin values *

- * Multiple headers are collapsed by {@code fn} into a single header entry delimited by commas (see - * RFC7230 Sec 3.2.2 for details), for example + * This will overwrite an existing header with an exact name match * - *

-     *     Accept: text/html
-     *     Accept: text/plain
-     * 
+ * @param key new header key + * @param v1 new header value + * @param vs more header values to set + * @return a new headers object with the specified header added + */ + public Headers setHeader(String key, String v1, String... vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(v1, "v1"); + Stream.of(vs).forEach((v) -> Objects.requireNonNull(v, "vs")); + + Map> nm = new HashMap<>(headers); + List s = new ArrayList<>(); + s.add(v1); + s.addAll(Arrays.asList(vs)); + nm.put(canonicalKey(key), Collections.unmodifiableList(s)); + return new Headers(Collections.unmodifiableMap(nm)); + } + + + /** + * Creates a new headers object with the specified headers set - this overwrites any existin values + *

+ * This will overwrite an existing header with an exact name match * - * is collapsed into + * @param key new header key + * @param vs header values to set + * @return a new headers object with the specified header added + */ + public Headers setHeader(String key, Collection vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(vs, "vs"); + if (vs.size() == 0) { + throw new IllegalArgumentException("can't set keys to an empty list"); + } + vs.forEach((v) -> Objects.requireNonNull(v, "vs")); + + Map> nm = new HashMap<>(headers); + nm.put(canonicalKey(key), Collections.unmodifiableList(new ArrayList<>(vs))); + return new Headers(Collections.unmodifiableMap(nm)); + + } + + /** + * Creates a new headers object with the specified headers remove - this overwrites any existin values + *

+ * This will overwrite an existing header with an exact name match * - *

-     *     Accept: text/html, text/plain
-     * 
+ * @param key new header key + * @return a new headers object with the specified header removed + */ + public Headers removeHeader(String key) { + Objects.requireNonNull(key, "key"); + + String canonKey = canonicalKey(key); + if (!headers.containsKey(canonKey)) { + return this; + } + + Map> nm = new HashMap<>(headers); + nm.remove(canonKey); + return new Headers(Collections.unmodifiableMap(nm)); + + } + + /** + * Returns the header matching the specified key. This matches headers in a case-insensitive way and substitutes + * underscore and hyphen characters such that : "CONTENT_TYPE_HEADER" and "Content-type" are equivalent. If no matching + * header is found then {@code Optional.empty} is returned. + *

+ * When multiple headers are present then the first value is returned- see { #getAllValues(String key)} to get all values for a header * * @param key match key * @return a header matching key or empty if no header matches. @@ -74,20 +246,60 @@ public Headers withHeader(String key, String value){ */ public Optional get(String key) { Objects.requireNonNull(key, "Key cannot be null"); - return getAll().entrySet().stream() - .filter((e) -> e.getKey() - .replaceAll("-", "_") - .equalsIgnoreCase(key.replaceAll("-", "_"))) - .map(Map.Entry::getValue) - .findFirst(); + String canonKey = canonicalKey(key); + + List val = headers.get(canonKey); + if (val == null){ + return Optional.empty(); + } + return Optional.of(val.get(0)); } /** - * The function invocation headers passed on the request + * Returns a collection of current header keys * - * @return a map of Invocation headers. + * @return a collection of keys */ - public Map getAll() { + public Collection keys() { + return headers.keySet(); + } + + /** + * Returns the headers as a map + * + * @return a map of key-values + */ + public Map> asMap() { return headers; } + + /** + * GetAllValues returns all values for a header or an empty list if the header has no values + * @param key the Header key + * @return a possibly empty list of values + */ + public List getAllValues(String key) { + return headers.getOrDefault(canonicalKey(key), Collections.emptyList()); + } + + public int hashCode() { + return headers.hashCode(); + } + + + public boolean equals(Object other) { + if (!(other instanceof Headers)) { + return false; + } + if (other == this) { + return true; + } + return headers.equals(((Headers) other).headers); + } + + @Override + public String toString() { + return Objects.toString(headers); + } + } diff --git a/api/src/main/java/com/fnproject/fn/api/InputBinding.java b/api/src/main/java/com/fnproject/fn/api/InputBinding.java index ddb2b009..a884cf88 100644 --- a/api/src/main/java/com/fnproject/fn/api/InputBinding.java +++ b/api/src/main/java/com/fnproject/fn/api/InputBinding.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.lang.annotation.ElementType; diff --git a/api/src/main/java/com/fnproject/fn/api/InputCoercion.java b/api/src/main/java/com/fnproject/fn/api/InputCoercion.java index f8e6af06..a5559105 100644 --- a/api/src/main/java/com/fnproject/fn/api/InputCoercion.java +++ b/api/src/main/java/com/fnproject/fn/api/InputCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.util.Optional; diff --git a/api/src/main/java/com/fnproject/fn/api/InputEvent.java b/api/src/main/java/com/fnproject/fn/api/InputEvent.java index 2ecab8aa..ef4e840a 100644 --- a/api/src/main/java/com/fnproject/fn/api/InputEvent.java +++ b/api/src/main/java/com/fnproject/fn/api/InputEvent.java @@ -1,7 +1,24 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.io.Closeable; import java.io.InputStream; +import java.time.Instant; import java.util.function.Function; public interface InputEvent extends Closeable { @@ -17,29 +34,21 @@ public interface InputEvent extends Closeable { */ T consumeBody(Function dest); - /** - * The application name associated with this function - * - * @return an application name - */ - String getAppName(); - /** - * @return The route (including preceding slash) of this function call - */ - String getRoute(); /** - * @return The full request URL of this function invocation + * return the current call ID for this event + * @return a call ID */ - String getRequestUrl(); + String getCallID(); + /** - * The HTTP method used to invoke this function + * The deadline by which this event should be processed - this is information and is intended to help you determine how long you should spend processing your event - if you exceed this deadline Fn will terminate your container. * - * @return an UpperCase HTTP method + * @return a deadline relative to the current system clock that the event must be processed by */ - String getMethod(); + Instant getDeadline(); /** @@ -49,11 +58,5 @@ public interface InputEvent extends Closeable { */ Headers getHeaders(); - /** - * The query parameters of the function invocation - * - * @return an immutable map of query parameters parsed from the request URL - */ - QueryParameters getQueryParameters(); } diff --git a/api/src/main/java/com/fnproject/fn/api/InvocationContext.java b/api/src/main/java/com/fnproject/fn/api/InvocationContext.java index 3b3395a1..245d582d 100644 --- a/api/src/main/java/com/fnproject/fn/api/InvocationContext.java +++ b/api/src/main/java/com/fnproject/fn/api/InvocationContext.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; @@ -9,6 +25,11 @@ */ public interface InvocationContext { + /** + * Returns the {@link RuntimeContext} associated with this invocation context + * + * @return a runtime context + */ RuntimeContext getRuntimeContext(); /** @@ -19,4 +40,39 @@ public interface InvocationContext { */ void addListener(InvocationListener listener); + + /** + * Returns the current request headers for the invocation + * + * @return the headers passed into the function + */ + Headers getRequestHeaders(); + + /** + * Sets the response content type, this will override the default content type of the output + * + * @param contentType a mime type for the response + */ + default void setResponseContentType(String contentType) { + this.setResponseHeader(OutputEvent.CONTENT_TYPE_HEADER, contentType); + } + + /** + * Adds a response header to the outbound event + * + * @param key header key + * @param value header value + */ + void addResponseHeader(String key, String value); + + /** + * Sets a response header to the outbound event, overriding a previous value. + *

+ * Headers set in this way override any headers returned by the function or any middleware on the function + * + * @param key header key + * @param v1 first value to set + * @param vs other values to set header to + */ + void setResponseHeader(String key, String v1, String... vs); } diff --git a/api/src/main/java/com/fnproject/fn/api/InvocationListener.java b/api/src/main/java/com/fnproject/fn/api/InvocationListener.java index 7c9d46fa..1e8b6b29 100644 --- a/api/src/main/java/com/fnproject/fn/api/InvocationListener.java +++ b/api/src/main/java/com/fnproject/fn/api/InvocationListener.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.util.EventListener; diff --git a/api/src/main/java/com/fnproject/fn/api/MethodWrapper.java b/api/src/main/java/com/fnproject/fn/api/MethodWrapper.java index 8ed0644f..655c28db 100644 --- a/api/src/main/java/com/fnproject/fn/api/MethodWrapper.java +++ b/api/src/main/java/com/fnproject/fn/api/MethodWrapper.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.lang.reflect.Method; diff --git a/api/src/main/java/com/fnproject/fn/api/OutputBinding.java b/api/src/main/java/com/fnproject/fn/api/OutputBinding.java index 1355e529..4bf05577 100644 --- a/api/src/main/java/com/fnproject/fn/api/OutputBinding.java +++ b/api/src/main/java/com/fnproject/fn/api/OutputBinding.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.lang.annotation.ElementType; diff --git a/api/src/main/java/com/fnproject/fn/api/OutputCoercion.java b/api/src/main/java/com/fnproject/fn/api/OutputCoercion.java index ac8e9ac4..44e3cc79 100644 --- a/api/src/main/java/com/fnproject/fn/api/OutputCoercion.java +++ b/api/src/main/java/com/fnproject/fn/api/OutputCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.util.Optional; diff --git a/api/src/main/java/com/fnproject/fn/api/OutputEvent.java b/api/src/main/java/com/fnproject/fn/api/OutputEvent.java index ef13983c..c6ab4143 100644 --- a/api/src/main/java/com/fnproject/fn/api/OutputEvent.java +++ b/api/src/main/java/com/fnproject/fn/api/OutputEvent.java @@ -1,47 +1,100 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.io.IOException; import java.io.OutputStream; +import java.util.Objects; import java.util.Optional; /** * Wrapper for an outgoing fn event */ public interface OutputEvent { + + String CONTENT_TYPE_HEADER = "Content-Type"; + + /** + * The outcome status of this function event + * This determines how the platform will reflect this error to the customer and how it will treat the container after an error + */ + enum Status { + /** + * The event was successfully processed + */ + Success(200), + /** + * The Function code raised unhandled exception + */ + FunctionError(502), + /** + * The Function code did not respond within a given timeout + */ + FunctionTimeout(504), + /** + * An internal error occurred in the FDK + */ + InternalError(500); + + private final int code; + + Status(int code) { + this.code = code; + } + + public int getCode() { + return this.code; + } + + } + + /** - * Report the HTTP status code of this event. - * For default-format functions, this value is mapped into a success/failure value as follows: - * status codes in the range [100, 400) are considered successful; anything else is a failure. + * Report the outcome status code of this event. * - * @return the status code associated with this event + * @return the status associated with this event */ - int getStatusCode(); + Status getStatus(); - int SUCCESS = 200; - int FAILURE = 500; /** * Report the boolean success of this event. * For default-format functions, this is used to map the HTTP status code into a straight success/failure. + * * @return true if the output event results from a successful invocation. */ default boolean isSuccess() { - return 100 <= getStatusCode() && getStatusCode() < 400; + return getStatus() == Status.Success; } /** - * The indicative content type of the response. + * The content type of the response. *

- * This will only be used when the function format is HTTP * * @return The name of the content type. */ - Optional getContentType(); + default Optional getContentType(){ + return getHeaders().get(CONTENT_TYPE_HEADER); + } /** * Any additional {@link Headers} that should be supplied along with the content - * + *

* These are only used when the function format is HTTP * * @return the headers to add @@ -51,51 +104,79 @@ default boolean isSuccess() { /** * Write the body of the output to a stream * - * @param out an outputstream to emit the body of the event - * @throws IOException OutputStream exceptions percolate up through this method + * @param out an outputstream to emit the body of the event + * @throws IOException OutputStream exceptions percolate up through this method */ void writeToOutput(OutputStream out) throws IOException; + /** + * Creates a new output event based on this one with the headers overriding + * @param headers the headers use in place of this event + * @return a new output event with these set + */ + default OutputEvent withHeaders(Headers headers) { + Objects.requireNonNull(headers, "headers"); + + OutputEvent a = this; + return new OutputEvent() { + + @Override + public Status getStatus() { + return a.getStatus(); + } + + @Override + public Headers getHeaders() { + return headers; + } + + @Override + public void writeToOutput(OutputStream out) throws IOException { + a.writeToOutput(out); + } + }; + } + /** * Create an output event from a byte array * * @param bytes the byte array to write to the output - * @param statusCode the status code to report + * @param status the status code to report * @param contentType the content type to present on HTTP responses * @return a new output event */ - static OutputEvent fromBytes(byte[] bytes, int statusCode, String contentType) { - return fromBytes(bytes, statusCode, contentType, Headers.emptyHeaders()); - } + static OutputEvent fromBytes(byte[] bytes, Status status, String contentType) { + return fromBytes(bytes, status, contentType, Headers.emptyHeaders()); + } /** * Create an output event from a byte array * * @param bytes the byte array to write to the output - * @param statusCode the HTTP status code of this event - * @param contentType the content type to present on HTTP responses + * @param status the status code of this event + * @param contentType the content type to present on HTTP responses or null * @param headers any additional headers to supply with HTTP responses * @return a new output event */ - static OutputEvent fromBytes(byte[] bytes, int statusCode, String contentType, Headers headers) { - if (statusCode < 100 || 600 <= statusCode) { - throw new IllegalArgumentException("Valid status codes must lie in the range [100, 599]"); - } + static OutputEvent fromBytes(final byte[] bytes, final Status status, final String contentType, final Headers headers) { + Objects.requireNonNull(bytes, "bytes"); + Objects.requireNonNull(status, "status"); + Objects.requireNonNull(headers, "headers"); + + final Headers newHeaders = contentType== null?Headers.emptyHeaders():headers.setHeader("Content-Type",contentType); return new OutputEvent() { @Override - public int getStatusCode() { - return statusCode; + public Status getStatus() { + return status; } - @Override - public Optional getContentType() { - return Optional.ofNullable(contentType); - } @Override - public Headers getHeaders() { return headers; } + public Headers getHeaders() { + return newHeaders; + } @Override public void writeToOutput(OutputStream out) throws IOException { @@ -104,24 +185,25 @@ public void writeToOutput(OutputStream out) throws IOException { }; } - static OutputEvent emptyResult(int statusCode) { - if (statusCode < 100 || 600 <= statusCode) { - throw new IllegalArgumentException("Valid status codes must lie in the range [100, 599]"); - } + /** + * Returns an output event with an empty body and a given status + * @param status the status of the event + * @return a new output event + */ + static OutputEvent emptyResult(final Status status) { + Objects.requireNonNull(status, "status"); + return new OutputEvent() { @Override - public int getStatusCode() { - return statusCode; + public Status getStatus() { + return status; } @Override - public Optional getContentType() { - return Optional.empty(); + public Headers getHeaders() { + return Headers.emptyHeaders(); } - @Override - public Headers getHeaders() { return Headers.emptyHeaders(); } - @Override public void writeToOutput(OutputStream out) throws IOException { diff --git a/api/src/main/java/com/fnproject/fn/api/QueryParameters.java b/api/src/main/java/com/fnproject/fn/api/QueryParameters.java index 08514054..0de6ca34 100644 --- a/api/src/main/java/com/fnproject/fn/api/QueryParameters.java +++ b/api/src/main/java/com/fnproject/fn/api/QueryParameters.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; import java.util.List; @@ -7,7 +23,7 @@ /** * Wrapper for query parameters map parsed from the URL of a function invocation. */ -public interface QueryParameters { +public interface QueryParameters { /** * Find the first entry for {@code key} if it exists otherwise returns {@code Optional.empty} * diff --git a/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java b/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java index 865a6d10..04936a8c 100644 --- a/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java +++ b/api/src/main/java/com/fnproject/fn/api/RuntimeContext.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; @@ -13,6 +29,38 @@ * of a function; they will not change between multiple invocations of a hot function. */ public interface RuntimeContext { + + + /** + * The application ID of the application associated with this function + * @return an application ID + */ + String getAppID(); + + /** + * THe function ID of the function + * @return a function ID + */ + String getFunctionID(); + + /** + * The user-friendly name of the application associated with this function, + * if present; defaulted to the application ID for backwards compatibility + * @return an application name + */ + default public String getAppName() { + return getAppID(); + } + + /** + * The user-friendly name of the function, if present; defaulted to the + * function ID for backwards compatibility + * @return a function name + */ + default public String getFunctionName() { + return getFunctionID(); + } + /** * Create an instance of the user specified class on which the target function to invoke is declared. * @@ -77,7 +125,7 @@ public interface RuntimeContext { * * @param targetMethod The user function method * @param param The index of the parameter - * @return a list of configured input coercions to apply to the given parameter + * @return a list of configured input coercions to apply to the given parameter */ List getInputCoercions(MethodWrapper targetMethod, int param); @@ -105,7 +153,21 @@ public interface RuntimeContext { * Set an {@link FunctionInvoker} for this function. The invoker will override * the built in function invoker, although the cloud threads invoker will still * have precedence so that cloud threads can be used from functions using custom invokers. + * * @param invoker The {@link FunctionInvoker} to add. + * @deprecated this is equivalent to {@link #addInvoker(FunctionInvoker, FunctionInvoker.Phase)} with a phase of {@link FunctionInvoker.Phase#Call} + */ + default void setInvoker(FunctionInvoker invoker) { + addInvoker(invoker, FunctionInvoker.Phase.Call); + } + + + /** + * Adds an FunctionInvoker handler to the runtime - new FunctionInvokers are added at the head of the specific phase they apply to so ordering may be important + * + * + * @param invoker an invoker to use to handle a given call + * @param phase the phase at which to add the invoke */ - void setInvoker(FunctionInvoker invoker); + void addInvoker(FunctionInvoker invoker, FunctionInvoker.Phase phase); } diff --git a/api/src/main/java/com/fnproject/fn/api/RuntimeFeature.java b/api/src/main/java/com/fnproject/fn/api/RuntimeFeature.java new file mode 100644 index 00000000..f57a3c1d --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/RuntimeFeature.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api; + +/** + * RuntimeFeatures are classes that configure the Fn Runtime prior to startup and can be loaded by annotating the function class with a {@link FnFeature} annotation + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public interface RuntimeFeature { + + /** + * Initialize the runtime context for this function + * + * @param context a runtime context to initalize + */ + void initialize(RuntimeContext context); +} diff --git a/api/src/main/java/com/fnproject/fn/api/TypeWrapper.java b/api/src/main/java/com/fnproject/fn/api/TypeWrapper.java index f453892d..b693446c 100644 --- a/api/src/main/java/com/fnproject/fn/api/TypeWrapper.java +++ b/api/src/main/java/com/fnproject/fn/api/TypeWrapper.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api; /** @@ -11,11 +27,11 @@ public interface TypeWrapper { * * For example, take the following classes: *

{@code
-     * class GenericParent<T> {
+     * class GenericParent {
      *   public void someMethod(T t) { // do something with t }
      * }
      *
-     * class ConcreteClass extends GenericParent<String> { }
+     * class ConcreteClass extends GenericParent { }
      * }
* * A {@link TypeWrapper} representing the first argument of {@code someMethod} would return {@code String.class} diff --git a/api/src/main/java/com/fnproject/fn/api/exception/FunctionConfigurationException.java b/api/src/main/java/com/fnproject/fn/api/exception/FunctionConfigurationException.java index e8c85889..6cb90cf9 100644 --- a/api/src/main/java/com/fnproject/fn/api/exception/FunctionConfigurationException.java +++ b/api/src/main/java/com/fnproject/fn/api/exception/FunctionConfigurationException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.exception; /** diff --git a/api/src/main/java/com/fnproject/fn/api/exception/FunctionInputHandlingException.java b/api/src/main/java/com/fnproject/fn/api/exception/FunctionInputHandlingException.java index 9762e6c4..984e871c 100644 --- a/api/src/main/java/com/fnproject/fn/api/exception/FunctionInputHandlingException.java +++ b/api/src/main/java/com/fnproject/fn/api/exception/FunctionInputHandlingException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.exception; /** diff --git a/api/src/main/java/com/fnproject/fn/api/exception/FunctionLoadException.java b/api/src/main/java/com/fnproject/fn/api/exception/FunctionLoadException.java index 095875cc..c911db0e 100644 --- a/api/src/main/java/com/fnproject/fn/api/exception/FunctionLoadException.java +++ b/api/src/main/java/com/fnproject/fn/api/exception/FunctionLoadException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.exception; /** diff --git a/api/src/main/java/com/fnproject/fn/api/exception/FunctionOutputHandlingException.java b/api/src/main/java/com/fnproject/fn/api/exception/FunctionOutputHandlingException.java index 9467f8ae..587b4aa3 100644 --- a/api/src/main/java/com/fnproject/fn/api/exception/FunctionOutputHandlingException.java +++ b/api/src/main/java/com/fnproject/fn/api/exception/FunctionOutputHandlingException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.exception; /** diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java b/api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java deleted file mode 100644 index db8ce8b6..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a function call failed within the fn platform - the function may or may not have been invoked and - * that invocation may or may not have completed. - */ -public class FunctionInvokeFailedException extends PlatformException { - public FunctionInvokeFailedException(String reason) { super(reason); } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java b/api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java deleted file mode 100644 index 5cf3eec9..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a function execution exceeds its configured timeout. - * - * When this exception is raised the fn server has terminated the container hosting the function. - */ -public class FunctionTimeoutException extends PlatformException { - public FunctionTimeoutException(String reason) { super(reason); } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java b/api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java deleted file mode 100644 index 8ec66fd6..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Enum representing the different HTTP types that can be used to invoke an external function - * - * @see Flow#invokeFunction - */ -public enum HttpMethod { - GET("GET"), - HEAD("HEAD"), - POST("POST"), - PUT("PUT"), - DELETE("DELETE"), - OPTIONS("OPTIONS"), - PATCH("PATCH"); - - private final String verb; - - HttpMethod(String verb) { - this.verb = verb; - } - - @Override - public String toString() { - return this.verb; - } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java b/api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java deleted file mode 100644 index 0f8e710a..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.fnproject.fn.api.flow; - -import com.fnproject.fn.api.Headers; - -/** - * An abstract HTTP request details (without location) - */ -public interface HttpRequest { - /** - * Return the HTTP method used to supply this value - * - * @return the HTTP method - */ - HttpMethod getMethod(); - - /** - * Return the headers on the HTTP request - * - * @return the headers - */ - Headers getHeaders(); - - /** - * Returns the body of the request as a byte array - * - * @return the function request body - */ - byte[] getBodyAsBytes(); -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java b/api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java deleted file mode 100644 index e3ef054a..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a completion stage responds with an incompatible datum type for its corresponding completion - * graph stage. - */ -public class InvalidStageResponseException extends PlatformException { - public InvalidStageResponseException(String reason) { super(reason); } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java b/api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java deleted file mode 100644 index d1085179..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a lambda or any referenced objects fail to be serialized. - * The cause will typically be a {@link java.io.NotSerializableException} or other {@link java.io.IOException} detailing what could not be serialized - */ -public class LambdaSerializationException extends FlowCompletionException { - public LambdaSerializationException(String message) { - super(message); - } - - public LambdaSerializationException(String message, Exception e) { - super(message, e); - } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java b/api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java deleted file mode 100644 index e2fad467..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a result returned by a completion stage fails to be serialized. - */ -public class ResultSerializationException extends FlowCompletionException { - public ResultSerializationException(String message, Throwable e) { - super(message, e); - } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java b/api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java deleted file mode 100644 index b4770bbc..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a a completion stage invocation failed within Fn - the stage may or may not have been invoked - * and that invocation may or may not have completed. - */ -public class StageInvokeFailedException extends PlatformException { - public StageInvokeFailedException(String reason) { super(reason); } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java b/api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java deleted file mode 100644 index 5131ff5f..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a stage failed after an internal error in the flow server, the stage may or may not have been - * invoked and that invocation may or may not have completed. - */ -public class StageLostException extends PlatformException { - public StageLostException(String reason) { super(reason); } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java b/api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java deleted file mode 100644 index 5de3c6d3..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.fnproject.fn.api.flow; - -/** - * Exception thrown when a completion stage function execution exceeds it configured timeout - - * the stage may or may not have completed normally. - * - * When this exception is raised the fn server has terminated the container hosting the function. - */ -public class StageTimeoutException extends PlatformException { - public StageTimeoutException(String reason) { super(reason); } -} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/package-info.java b/api/src/main/java/com/fnproject/fn/api/flow/package-info.java deleted file mode 100644 index 0d21dcaf..00000000 --- a/api/src/main/java/com/fnproject/fn/api/flow/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * SDK for creating and running asynchronous processes from within fn for Java. - */ -package com.fnproject.fn.api.flow; diff --git a/api/src/main/java/com/fnproject/fn/api/httpgateway/HTTPGatewayContext.java b/api/src/main/java/com/fnproject/fn/api/httpgateway/HTTPGatewayContext.java new file mode 100644 index 00000000..fdbde96b --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/httpgateway/HTTPGatewayContext.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.httpgateway; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.QueryParameters; + +/** + * A context for accessing and setting HTTP Gateway atributes such aas headers and query parameters from a function call + *

+ * Created on 19/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public interface HTTPGatewayContext { + + /** + * Returns the underlying invocation context behind this HTTP context + * + * @return an invocation context related to this function + */ + InvocationContext getInvocationContext(); + + + /** + * Returns the HTTP headers for the request associated with this function call + * If no headers were set this will return an empty headers object + * + * @return the incoming HTTP headers sent in the gateway request + */ + Headers getHeaders(); + + + /** + * Returns the fully qualified request URI that the function was called with, including query parameters + * + * @return the request URI of the function + */ + String getRequestURL(); + + + /** + * Returns the incoming request method for the HTTP + * + * @return the HTTP method set on this call + */ + String getMethod(); + + /** + * Returns the query parameters of the request + * + * @return a query parameters object + */ + QueryParameters getQueryParameters(); + + + /** + * Adds a response header to the outbound event + * + * @param key header key + * @param value header value + */ + void addResponseHeader(String key, String value); + + /** + * Sets a response header to the outbound event, overriding a previous value. + *

+ * Headers set in this way override any headers returned by the function or any middleware on the function + *

+ * Setting the "Content-Type" response header also sets this on the underlying Invocation context + * + * @param key header key + * @param v1 first value to set + * @param vs other values to set header to + */ + void setResponseHeader(String key, String v1, String... vs); + + /** + * Sets the HTTP status code of the response + * + * @param code an HTTP status code + * @throws IllegalArgumentException if the code is < 100 or >l=600 + */ + void setStatusCode(int code); +} diff --git a/api/src/main/java/com/fnproject/fn/api/tracing/TracingContext.java b/api/src/main/java/com/fnproject/fn/api/tracing/TracingContext.java new file mode 100644 index 00000000..36ac377c --- /dev/null +++ b/api/src/main/java/com/fnproject/fn/api/tracing/TracingContext.java @@ -0,0 +1,101 @@ +package com.fnproject.fn.api.tracing; + +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.RuntimeContext; + +public interface TracingContext { + + /** + * Returns the underlying invocation context behind this Tracing context + * + * @return an invocation context related to this function + */ + InvocationContext getInvocationContext(); + + /** + * Returns the {@link RuntimeContext} associated with this invocation context + * + * @return a runtime context + */ + RuntimeContext getRuntimeContext(); + + /** + * Returns true if tracing is enabled for this function invocation + * + * @return whether tracing is enabled + */ + Boolean isTracingEnabled(); + + /** + * Returns the user-friendly name of the application associated with the + * function; shorthand for getRuntimeContext().getAppName() + * + * @return the user-friendly name of the application associated with the + * function + */ + String getAppName(); + + /** + * Returns the user-friendly name of the function; shorthand for + * getRuntimeContext().getFunctionName() + * + * @return the user-friendly name of the function + */ + String getFunctionName(); + + /** + * Returns a standard constructed "service name" to be used in tracing + * libraries to identify the function + * + * @return a standard constructed "service name" + */ + String getServiceName(); + + /** + * Returns the URL to be used in tracing libraries as the destination for + * the tracing data + * + * @return a string containing the trace collector URL + */ + String getTraceCollectorURL(); + + /** + * Returns the current trace ID as extracted from Zipkin B3 headers if they + * are present on the request + * + * @return the trace ID as a string + */ + String getTraceId(); + + /** + * Returns the current span ID as extracted from Zipkin B3 headers if they + * are present on the request + * + * @return the span ID as a string + */ + String getSpanId(); + + /** + * Returns the parent span ID as extracted from Zipkin B3 headers if they + * are present on the request + * + * @return the parent span ID as a string + */ + String getParentSpanId(); + + /** + * Returns the value of the Sampled header of the Zipkin B3 headers if they + * are present on the request + * + * @return true if sampling is enabled for the request + */ + Boolean isSampled(); + + /** + * Returns the value of the Flags header of the Zipkin B3 headers if they + * are present on the request + * + * @return the verbatim value of the X-B3-Flags header + */ + String getFlags(); +} diff --git a/api/src/test/java/com/fnproject/fn/api/HeadersTest.java b/api/src/test/java/com/fnproject/fn/api/HeadersTest.java new file mode 100644 index 00000000..7143c34a --- /dev/null +++ b/api/src/test/java/com/fnproject/fn/api/HeadersTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class HeadersTest { + + @Test + public void shouldCanonicalizeHeaders(){ + for (String[] v : new String[][] { + {"",""}, + {"a","A"}, + {"fn-ID-","Fn-Id-"}, + {"myHeader-VaLue","Myheader-Value"}, + {" Not a Header "," Not a Header "}, + {"-","-"}, + {"--","--"}, + {"a-","A-"}, + {"-a","-A"} + }){ + assertThat(Headers.canonicalKey(v[0])).isEqualTo(v[1]); + } + } + + +} diff --git a/bin/scripts/migration/flag b/bin/scripts/migration/flag new file mode 100644 index 00000000..4a3b60a2 --- /dev/null +++ b/bin/scripts/migration/flag @@ -0,0 +1 @@ +0 0 \ No newline at end of file diff --git a/bin/scripts/migration/migrate_to_maven_central.sh b/bin/scripts/migration/migrate_to_maven_central.sh new file mode 100644 index 00000000..5be056e0 --- /dev/null +++ b/bin/scripts/migration/migrate_to_maven_central.sh @@ -0,0 +1,395 @@ +#!/bin/bash + +set -euo pipefail + +destdir=flag +POM_PLACEHOLDER=".*>" +POM_REPLACEMENT="4.0.0 + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + oracle + https://www.oracle.com/ + + + + fnproject/core + roneet.shaw@oracle.com + oracle + https://www.oracle.com/ + + + + scm:git:git://github.com/fnproject/fdk-java.git + scm:git:ssh://github.com:fnproject/fdk-java.git + https://github.com/fnproject/fdk-java + + + + central + https://central.sonatype.com/api/v1/staging/deploy/maven2 + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + + fdk-java + The Function Development Kit for Java makes it easy to build and deploy Java functions to Fn + https://fnproject.io/tutorials/JavaFDKIntroduction/ + " + +# Constants +MAVEN_CENTRAL_STAGINGURL="https://central.sonatype.com/api/v1/staging/deploy/maven2" +MAVEN_CENTRAL_REPOID="central" +OUTPUT_DIR="output" + +# TODO: add versions e.g. (1.0.0 1.0.1 1.0.2) 10*1+ 5+2+1 + +VERSIONS=(1.0.107 1.0.106 1.0.105 1.0.104 1.0.103 1.0.102 1.0.101 1.0.100 1.0.99 1.0.98 1.0.97 1.0.96 1.0.95 1.0.94 1.0.93 1.0.92 1.0.91 1.0.90 1.0.89 1.0.88) + +#VERSIONS=(1.0.127 1.0.87 1.0.86 1.0.85 1.0.84 1.0.83 1.0.82 1.0.81 1.0.80 1.0.79 1.0.78 1.0.77 1.0.76 1.0.75 1.0.74 1.0.72 1.0.71 1.0.70 1.0.64 1.0.63 1.0.62 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 1.0.55 1.0.54 1.0.53 1.0.52 1.0.51 1.0.50 1.0.49 1.0.48 1.0.47 1.0.46 1.0.45 1.0.44 1.0.43 1.0.42 1.0.41 1.0.40 1.0.39 1.0.38 1.0.37 1.0.36 1.0.35 1.0.34 1.0.33 1.0.32 1.0.31 1.0.30 1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 1.0.18 1.0.17 1.0.16 1.0.15 1.0.14 1.0.13 1.0.11 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1) + +#VERSIONS=(1.0.127 1.0.125 1.0.124 1.0.123 1.0.122 1.0.121 1.0.120 1.0.119 1.0.118 1.0.117 1.0.116 1.0.115 1.0.114 1.0.113 1.0.112 1.0.111 1.0.110 1.0.109 1.0.108 1.0.107 1.0.106 1.0.105 1.0.104 1.0.103 1.0.102 1.0.101 1.0.100 1.0.99 1.0.98 1.0.97 1.0.96 1.0.95 1.0.94 1.0.93 1.0.92 1.0.91 1.0.90 1.0.89 1.0.88 1.0.87 1.0.86 1.0.85 1.0.84 1.0.83 1.0.82 1.0.81 1.0.80 1.0.79 1.0.78 1.0.77 1.0.76 1.0.75 1.0.74 1.0.72 1.0.71 1.0.70 1.0.64 1.0.63 1.0.62 1.0.61 1.0.60 1.0.59 1.0.58 1.0.57 1.0.56 1.0.55 1.0.54 1.0.53 1.0.52 1.0.51 1.0.50 1.0.49 1.0.48 1.0.47 1.0.46 1.0.45 1.0.44 1.0.43 1.0.42 1.0.41 1.0.40 1.0.39 1.0.38 1.0.37 1.0.36 1.0.35 1.0.34 1.0.33 1.0.32 1.0.31 1.0.30 1.0.29 1.0.28 1.0.27 1.0.26 1.0.25 1.0.24 1.0.23 1.0.22 1.0.21 1.0.20 1.0.19 1.0.18 1.0.17 1.0.16 1.0.15 1.0.14 1.0.13 1.0.11 1.0.10 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0) + +# TODO: https://dl.bintray.com//// +BINTRAYURL="http://dl.bintray.com/fnproject/fnproject/com/fnproject/fn/" + +#IFS=$' ' read -d '' -r -a START_VER < $destdir + +IFS=$' ' GLOBIGNORE='*' command eval 'START_VER=($(cat flag))' + +# TODO : add artifact Id +ARTIFACT_IDs=(fdk api experimental-native-image-support flow-api flow-runtime flow-testing fn-spring-cloud-function jrestless-handler runtime testing-core testing-junit4 testing) +#ARTIFACT_IDs=(fdk api ) + + +#parameters $1: url to test +function ping_url() { + status_code=$(curl --head --write-out '%{http_code}\n' --silent --output /dev/null $1) + echo $status_code +} + +# Utilities +function escape_pom() { + echo "$1" | sed 's#/#\\/#g' | tr '\n' '@' +} + +function xml_encode() { + echo $1 | sed 's/&/\&/g; s//\>/g; s/"/\"/g; s/'"'"'/\'/g' +} + +#parameters $1: artifact_id, $2: version +function download_and_save(){ + #copy javadoc + + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2-javadoc.jar ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2-javadoc.jar \ + -o $OUTPUT_DIR/$1/$2/$1-$2-javadoc.jar + fi + + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2-javadoc.jar.md5 ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2-javadoc.jar.md5 \ + -o $OUTPUT_DIR/$1/$2/$1-$2-javadoc.jar.md5 + fi + + #copy sources + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2-sources.jar ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2-sources.jar \ + -o $OUTPUT_DIR/$1/$2/$1-$2-sources.jar + fi + + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2-sources.jar.md5 ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2-sources.jar.md5 \ + -o $OUTPUT_DIR/$1/$2/$1-$2-sources.jar.md5 + fi + + #copy jar + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2.jar ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2.jar \ + -o $OUTPUT_DIR/$1/$2/$1-$2.jar + fi + + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2.jar.md5 ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2.jar.md5 \ + -o $OUTPUT_DIR/$1/$2/$1-$2.jar.md5 + fi + + #copy pom + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2.pom ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2.pom \ + -o $OUTPUT_DIR/$1/$2/$1-$2.pom + fi + + if [ $( ping_url $BINTRAYURL/$1/$2/$1-$2.pom.md5 ) == 200 ] + then + mkdir -p $OUTPUT_DIR/$1/$2 + curl -L $BINTRAYURL/$1/$2/$1-$2.pom.md5 \ + -o $OUTPUT_DIR/$1/$2/$1-$2.pom.md5 + fi + +} + +function save_flag() { + echo "$1 $2" > "$destdir" +} + +function download_all_versions(){ + for (( i=${START_VER[0]}; i<${#ARTIFACT_IDs[@]}; i++ )); do + for (( j=${START_VER[1]}; j<${#VERSIONS[@]}; j++ )); do + aid=${ARTIFACT_IDs[$i]} + ver=${VERSIONS[$j]} + + echo "Downloading version $ver $aid" + download_and_save $aid $ver + save_flag $i $j + done + START_VER[1]=0 + done +} + +function prepare_pom() { + echo "Preparing $1" + EXAMPLE_MODULE="examples<\/module>" + EXP_MODULE="integration-tests<\/module>" + NEW=" " + PLUGINS="" + SCM_PLUGIN=" + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + true + published + + + + " + for (( i=${START_VER[0]}; i<${#ARTIFACT_IDs[@]}; i++ )); do + for (( j=${START_VER[1]}; j<${#VERSIONS[@]}; j++ )); do + aid=${ARTIFACT_IDs[$i]} + ver=${VERSIONS[$j]} + + echo $aid $ver + #if [ $ver = $2 ]; then + pom=$OUTPUT_DIR/$aid/$ver/$aid-$ver.pom + if [ -f "$pom" ]; then + if [ $aid = "fdk" ]; then + newPom=$1/$aid/$ver/pom.xml + mkdir -p $1/$aid/$ver + sed -e "s/$POM_PLACEHOLDER/$(escape_pom "$POM_REPLACEMENT")/g; s/$EXAMPLE_MODULE/$NEW/g; s/$EXP_MODULE/$NEW/g; s/$PLUGINS/$(escape_pom "$SCM_PLUGIN")/1;" \ + $pom |\ + tr '@' '\n' > temp.txt + mv temp.txt $newPom + else + newPom=$1/fdk/$ver/$aid/pom.xml + mkdir -p $1/fdk/$ver/$aid + + #Copy pom + cp $pom $1/fdk/$ver/$aid/ + mv $1/fdk/$ver/$aid/$aid-$ver.pom $newPom + fi + fi + #fi + done + done + +} + + +function modify_group_id() { + echo "Changing $1" + OLD_GROUP_ID="com.fnproject.fn<\/groupId>" + NEW_GROUP_ID="$1" + EXAMPLE_MODULE="examples<\/module>" + EXP_MODULE="integration-tests<\/module>" + NEW=" " + + PLUGINS="" + SCM_PLUGIN=" + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + true + published + + + + " + for (( i=${START_VER[0]}; i<${#ARTIFACT_IDs[@]}; i++ )); do + for (( j=${START_VER[1]}; j<${#VERSIONS[@]}; j++ )); do + aid=${ARTIFACT_IDs[$i]} + ver=${VERSIONS[$j]} + + echo $aid $ver + if [ $ver = $2 ]; then + pom=$OUTPUT_DIR/$aid/$ver/$aid-$ver.pom + if [ -f "$pom" ]; then + if [ $aid = "fdk" ]; then + newPom=$1/$aid/$ver/pom.xml + mkdir -p $1/$aid/$ver + sed -e "s/$OLD_GROUP_ID/$(escape_pom "$NEW_GROUP_ID")/g; s/$POM_PLACEHOLDER/$(escape_pom "$POM_REPLACEMENT")/g; s/$EXAMPLE_MODULE/$NEW/g; s/$EXP_MODULE/$NEW/g; s/$PLUGINS/$(escape_pom "$SCM_PLUGIN")/1;" \ + $pom |\ + tr '@' '\n' > temp.txt + mv temp.txt $newPom + else + newPom=$1/fdk/$ver/$aid/pom.xml + mkdir -p $1/fdk/$ver/$aid + sed -e "s/$OLD_GROUP_ID/$(escape_pom "$NEW_GROUP_ID")/g" \ + $pom |\ + tr '@' '\n' > temp.txt + mv temp.txt $newPom + fi + fi + fi + done + done + +} + +function deploy() { + for (( i=${START_VER[0]}; i<${#ARTIFACT_IDs[@]}; i++ )); do + for (( j=${START_VER[1]}; j<${#VERSIONS[@]}; j++ )); do + aid=${ARTIFACT_IDs[$i]} + ver=${VERSIONS[$j]} + + #if [ $ver = $2 ]; then + # Add required metadata to pom.xml + if [ $aid = "fdk" ]; then + pom=$1/$aid/$ver/pom.xml + if [ -f "$pom" ]; then + echo "pom maven fdk" + echo $pom + mvn -s ./user-settings.xml gpg:sign-and-deploy-file \ + -Durl=$MAVEN_CENTRAL_STAGINGURL \ + -DrepositoryId=$MAVEN_CENTRAL_REPOID \ + -DpomFile=$pom \ + -Dfile=$pom + fi + else + pom=$1/fdk/$ver/$aid/pom.xml + if [ -f "$pom" ]; then + echo "pom maven other" + mvn -s ./user-settings.xml gpg:sign-and-deploy-file \ + -Durl=$MAVEN_CENTRAL_STAGINGURL \ + -DrepositoryId=$MAVEN_CENTRAL_REPOID \ + -DpomFile=$pom \ + -Dfile=$pom + fi + echo "Singing jar" + echo $pom + + #sign jar files + if [ -f "$pom" ] && [ -f $OUTPUT_DIR/$aid/$ver/$aid-$ver.jar ]; then + echo "After jar inside if" + echo $pom + mvn -s ./user-settings.xml gpg:sign-and-deploy-file \ + -Durl=$MAVEN_CENTRAL_STAGINGURL \ + -DrepositoryId=$MAVEN_CENTRAL_REPOID \ + -DpomFile=$pom \ + -Dfile=$OUTPUT_DIR/$aid/$ver/$aid-$ver.jar + fi + + echo "Singing sources" + echo $pom + #Sign sources.jar + if [ -f "$pom" ] && [ -f $OUTPUT_DIR/$aid/$ver/$aid-$ver-sources.jar ]; then + echo "sources" + mvn -s ./user-settings.xml gpg:sign-and-deploy-file \ + -Durl=$MAVEN_CENTRAL_STAGINGURL \ + -DrepositoryId=$MAVEN_CENTRAL_REPOID \ + -DpomFile=$pom \ + -Dfile=$OUTPUT_DIR/$aid/$ver/$aid-$ver-sources.jar \ + -Dclassifier=sources + elif [ -d $OUTPUT_DIR/$aid/$ver ] && [ $aid != "fdk" ]; then + if [ -f fallback/$aid-sources.jar ]; then + echo "Fallback sourcess" + echo $OUTPUT_DIR/$aid/$ver + cp fallback/$aid-sources.jar $1/fdk/$ver/$aid/ + mv $1/fdk/$ver/$aid/$aid-sources.jar $1/fdk/$ver/$aid/$aid-$ver-sources.jar + mvn -s ./user-settings.xml gpg:sign-and-deploy-file \ + -Durl=$MAVEN_CENTRAL_STAGINGURL \ + -DrepositoryId=$MAVEN_CENTRAL_REPOID \ + -DpomFile=$pom \ + -Dfile=$1/fdk/$ver/$aid/$aid-$ver-sources.jar \ + -Dclassifier=sources + fi + fi + echo "Singing Javadocs" + echo $pom + + #sign javadoc.jar + if [ -f "$pom" ] && [ -f $OUTPUT_DIR/$aid/$ver/$aid-$ver-javadoc.jar ]; then + echo "javadocs" + mvn -s ./user-settings.xml gpg:sign-and-deploy-file \ + -Durl=$MAVEN_CENTRAL_STAGINGURL \ + -DrepositoryId=$MAVEN_CENTRAL_REPOID \ + -DpomFile=$pom \ + -Dfile=$OUTPUT_DIR/$aid/$ver/$aid-$ver-javadoc.jar \ + -Dclassifier=javadoc + elif [ -d $OUTPUT_DIR/$aid/$ver ] && [ $aid != "fdk" ]; then + if [ -f fallback/$aid-javadoc.jar ]; then + echo "Fallback javadocs2" + echo $OUTPUT_DIR/$aid/$ver + cp fallback/$aid-javadoc.jar $1/fdk/$ver/$aid/ + mv $1/fdk/$ver/$aid/$aid-javadoc.jar $1/fdk/$ver/$aid/$aid-$ver-javadoc.jar + mvn -s ./user-settings.xml gpg:sign-and-deploy-file \ + -Durl=$MAVEN_CENTRAL_STAGINGURL \ + -DrepositoryId=$MAVEN_CENTRAL_REPOID \ + -DpomFile=$pom \ + -Dfile=$1/fdk/$ver/$aid/$aid-$ver-javadoc.jar \ + -Dclassifier=javadoc + fi + fi + fi + #fi + #save_flag $i $j + done + START_VER[1]=0 + done +} + +#main +#Script execution will start from here +if [[ $1 = "-d" ]]; then + echo "Downloading fdk-java version from Bintray" + download_all_versions + save_flag $i $j +elif [[ $1 = "-c" ]]; then + echo "Changing groupId to $2" + modify_group_id $2 $3 + deploy $2 $3 +else + echo "Sign and Deploy to Maven Central" + echo $1 + prepare_pom $1 + deploy $1 +fi +exit 0 diff --git a/build-image/Dockerfile b/build-image/Dockerfile deleted file mode 100644 index 003c4107..00000000 --- a/build-image/Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM maven:3-jdk-8-slim - -ADD pom.xml /tmp/cache-deps/pom.xml -ADD cache-deps.sh /tmp/cache-deps/cache-deps.sh -ADD src /tmp/cache-deps/src - -RUN /tmp/cache-deps/cache-deps.sh diff --git a/build-image/Dockerfile-jdk9 b/build-image/Dockerfile-jdk9 deleted file mode 100644 index db4b8917..00000000 --- a/build-image/Dockerfile-jdk9 +++ /dev/null @@ -1,7 +0,0 @@ -FROM maven:3-jdk-9-slim - -ADD pom.xml /tmp/cache-deps/pom.xml -ADD cache-deps.sh /tmp/cache-deps/cache-deps.sh -ADD src /tmp/cache-deps/src - -RUN /tmp/cache-deps/cache-deps.sh diff --git a/build-image/cache-deps.sh b/build-image/cache-deps.sh deleted file mode 100755 index c3a31654..00000000 --- a/build-image/cache-deps.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -ex - -cd /tmp/cache-deps && mvn test package dependency:copy-dependencies -Dmaven.repo.local=/usr/share/maven/ref/repository -Dmdep.prependGroupId=true -DoutputDirectory=target -cd / && rm -fr /tmp/cache-deps diff --git a/build-image/docker-build.sh b/build-image/docker-build.sh deleted file mode 100755 index 494b2cd7..00000000 --- a/build-image/docker-build.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -ex - -cd /tmp/staging-repository && python -mSimpleHTTPServer 18080 1>>/tmp/http-logs 2>&1 & -SRV_PROCESS=$! - -docker build $* -kill $SRV_PROCESS diff --git a/build-image/pom.xml b/build-image/pom.xml deleted file mode 100644 index 6ae2415e..00000000 --- a/build-image/pom.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - 4.0.0 - - - - UTF-8 - 1.0.0-SNAPSHOT - - - - - com.fnproject.fn - build-image - 1.0.0 - - - - com.fnproject.fn - api - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - - fn-maven-releases - http://172.17.0.1:18080 - - - diff --git a/build-image/src/main/java/com/example/fn/HelloFunction.java b/build-image/src/main/java/com/example/fn/HelloFunction.java deleted file mode 100644 index 8c581e76..00000000 --- a/build-image/src/main/java/com/example/fn/HelloFunction.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.example.fn; - -public class HelloFunction { - - public String handleRequest(String input) { - String name = (input == null || input.isEmpty()) ? "world" : input; - - return "Hello, " + name + "!"; - } - -} \ No newline at end of file diff --git a/build-image/src/test/java/com/example/fn/HelloFunctionTest.java b/build-image/src/test/java/com/example/fn/HelloFunctionTest.java deleted file mode 100644 index e6b7a5e3..00000000 --- a/build-image/src/test/java/com/example/fn/HelloFunctionTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.fn; - -import com.fnproject.fn.testing.*; -import org.junit.*; - -import static org.junit.Assert.*; - -public class HelloFunctionTest { - - @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); - - @Test - public void shouldReturnGreeting() { - testing.givenEvent().enqueue(); - testing.thenRun(HelloFunction.class, "handleRequest"); - - FnResult result = testing.getOnlyResult(); - assertEquals("Hello, world!", result.getBodyAsString()); - } - -} \ No newline at end of file diff --git a/build_in_docker.sh b/build_in_docker.sh new file mode 100644 index 00000000..2081b9fc --- /dev/null +++ b/build_in_docker.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +docker run --rm --name compile -v "$(pwd)":/usr/src/mymaven -w /usr/src/mymaven svenruppert/maven-3.5-jdk-08 mvn clean install diff --git a/docs/ExtendingDataBinding.md b/docs/ExtendingDataBinding.md index bc8195c9..26a8a2d2 100644 --- a/docs/ExtendingDataBinding.md +++ b/docs/ExtendingDataBinding.md @@ -19,7 +19,7 @@ First of all, let's create a new function project. If you haven't done it alread ```shell $ fn start & -$ fn apps create java-app +$ fn create app java-app Successfully created app: java-app ``` diff --git a/docs/FAQ.md b/docs/FAQ.md index 375650b5..4dfcb4aa 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -12,33 +12,16 @@ The FDK is comprised of: - a build-time Docker image for repeatable builds. ### Is the FDK required in order to run Java on Fn? -No. You can still write Java functions on Fn without using the FDK. However using the FDK will make several things easier for you: - 1. A curated base image for Java 8 and Java 9 means that you don't have to build and maintain your own image. These images contain optimizations for quick JVM startup times. - 1. Accessing configuration from Fn is easy through FDK APIs. - 1. Input and output type coercion reduces the amount of serialization and formatting boilerplate that you have to write. - 1. A JUnit rule provides a realistic test harness for you to test your function in isolation. +Yes - The FDK implements the IO/contract with the FN service and is required to receive events from the platform ### What is Fn Flow? Fn Flow is a [Java API](https://github.com/fnproject/fn-java-fdk/blob/master/docs/FnFlowsUserGuide.md) and [corresponding service](https://github.com/fnproject/flow) that helps you create complex, long-running, fault-tolerant functions using a promises-style asynchronous API. Check out the [Fn Flow docs](https://github.com/fnproject/fn-java-fdk/blob/master/docs/FnFlowsUserGuide.md) for more information. ### How do I get the FDK? -The FDK is automatically added to your project if you built your function using `fn init --runtime=java`. The `api` and `testing` JARs are published on [our bintray](https://bintray.com/fnproject/fnproject) and the `runtime` is published in our [Docker hub repository](https://hub.docker.com/r/fnproject/fn-java-fdk/). - +The FDK is automatically added to your project if you built your function using `fn init --runtime=java`. The `api` and `testing` JARs are published on [our maven central repository](https://search.maven.org/search?q=g:com.fnproject.fn) and the `runtime` is published in our [Docker hub repository](https://hub.docker.com/r/fnproject/fn-java-fdk/). ### How do I add the FDK to an existing project? - 1. Find the latest release from the [releases page](https://github.com/fnproject/fn-java-fdk/releases). For example `1.0.32`. - 1. The FDK JAR is published on [Bintray](https://bintray.com/fnproject/fnproject). Add the repository to your`pom.xml` `repositories` section: - ```xml - - fn-release-repo - https://dl.bintray.com/fnproject/fnproject - - true - - - false - - - ``` + 1. Find the latest release from the [releases page](https://github.com/fnproject/fn-java-fdk/releases). For example `1.0.149`. + 1. The FDK JAR is published on [Maven Central](https://search.maven.org/search?q=g:com.fnproject.fn). Add the repository to your`pom.xml` `repositories` section: 1. Add the dependency to your `dependency` section. Make sure that the `version` tag matches the latest release that you looked up above. ```xml diff --git a/docs/FnFlowsAdvancedTopics.md b/docs/FnFlowsAdvancedTopics.md index ac2f75a5..efd40e6d 100644 --- a/docs/FnFlowsAdvancedTopics.md +++ b/docs/FnFlowsAdvancedTopics.md @@ -61,6 +61,7 @@ An important consideration is that, if your lambda captures fields from your function class, then that class must also be Serializable: ```java +@FnFeature(FlowFeature.class) public class MyFunction{ private String config = "foo"; @@ -82,6 +83,7 @@ E.g. making `MyFunction` serializable will work as the function instance object will be captured alongside the lambda: ```java +@FnFeature(FlowFeature.class) public class MyFunction implements Serializable{ private String config = "foo"; @@ -104,6 +106,7 @@ prior to passing them, removing the need to make the function class serializable. For example: ```java +@FnFeature(FlowFeature.class) public class MyFunction{ private final Database db; // non-serializable object private final String config = "foo"; @@ -128,6 +131,7 @@ Alternatively, you can make non-serializable fields `transient` and construct them on the fly: ```java +@FnFeature(FlowFeature.class) public class MyFunction implements Serialiable{ private final transient Database db; // non-serializable object private final String config = "foo"; @@ -298,6 +302,7 @@ exception. E.g.: ```java +@FnFeature(FlowFeature.class) public class MyFunction{ public static class MyException extends RuntimeException{ public MyException(String message){ diff --git a/docs/FnFlowsUserGuide.md b/docs/FnFlowsUserGuide.md index 91ed4ca4..7834fe0c 100644 --- a/docs/FnFlowsUserGuide.md +++ b/docs/FnFlowsUserGuide.md @@ -60,7 +60,7 @@ $ fn start Similarly, start the Flows server server and point it at the functions server API URL: ``` -$ DOCKER_LOCALHOST=$(docker inspect --type container -f '{{.NetworkSettings.Gateway}}' functions) +$ DOCKER_LOCALHOST=$(docker inspect --type container -f '{{.NetworkSettings.Gateway}}' fnserver) $ docker run --rm \ -p 8081:8081 \ @@ -99,6 +99,19 @@ func.yaml created ``` +### Add the Flow runtime to your function + +In your `pom.xml` add a depdendency on `flow-runtime` : + +```$ml + + com.fnproject.fn + flow-runtime + ${fdk.version} + + +``` + ### Create a Flow within your Function You will create a function that produces the nth prime number and then returns @@ -117,7 +130,10 @@ package com.example.fn; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.api.FnFeature; +@FnFeature(FlowFeature.class) public class PrimeFunction { public String handleRequest(int nth) { @@ -166,7 +182,7 @@ path: /primes Create your app and deploy your function: ``` -$ fn apps create flows-example +$ fn create app flows-example Successfully created app: flows-example $ fn deploy --app flows-example @@ -178,20 +194,27 @@ Configure your function to talk to the local flow service endpoint: ``` $ DOCKER_LOCALHOST=$(docker inspect --type container -f '{{.NetworkSettings.Gateway}}' functions) -$ fn apps config set flows-example COMPLETER_BASE_URL "http://$DOCKER_LOCALHOST:8081" +$ fn config app flows-example COMPLETER_BASE_URL "http://$DOCKER_LOCALHOST:8081" ``` ### Run your Flow function -You can now run your function using `fn call` or HTTP and curl: +You can now run your function using `fn invoke` or HTTP. ``` -$ echo 10 | fn call flows-example /primes +$ echo 10 | fn invoke flows-example primes The 10th prime number is 29 ``` +To invoke your function via HTTP, you need to know its invocation endpoint (or the function needs to have an HTTP trigger defined). + +``` +$ fn inspect fn flows-examples primes ``` -$ curl -XPOST -d "10" http://localhost:8080/r/flows-example/primes + +Take note of the `fnproject.io/fn/invokeEndpoint` URL and invoke it (ex. using curl). + +$ curl -X POST -d "10" http://localhost:8080/invoke/... The 10th prime number is 29 ``` diff --git a/docs/HTTPGatewayFunctions.md b/docs/HTTPGatewayFunctions.md new file mode 100644 index 00000000..86afd87a --- /dev/null +++ b/docs/HTTPGatewayFunctions.md @@ -0,0 +1,36 @@ +# Accessing HTTP Information From Functions + +Functions can be used to handle events, RPC calls or HTTP requests. When you are writing a function that handles an HTTP request you frequently need access to the HTTP headers of the incoming request or need to set HTTP headers or the status code on the outbound respsonse. + + +In Fn for Java, when your function is being served by an HTTP trigger (or another compatible HTTP gateway) you can get access to both the incoming request headers for your function by adding a 'com.fnproject.fn.api.httpgateway.HTTPGatewayContext' parameter to your function's parameters. + + + Using this allows you to : + + * Read incoming headers + * Access the method and request URL for the trigger + * Write outbound headers to the response + * Set the status code of the response + + + For example this function reads a request header the method and request URL, sets an response header and sets the response status code to perform an HTTP redirect. + +```java +package com.fnproject.fn.examples; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; + + +public class RedirectFunction { + + public void redirect(HTTPGatewayContext hctx) { + System.err.println("Request URL is:" + hctx.getRequestURL()); + System.err.println("Trace ID" + hctx.getHeaders().get("My-Trace-ID").orElse("N/A")); + + hctx.setResponseHeader("Location","http://example.com"); + hctx.setStatusCode(302); + + } +} + +``` diff --git a/docs/TestingFunctions.md b/docs/TestingFunctions.md index ffabd74d..9127b16e 100644 --- a/docs/TestingFunctions.md +++ b/docs/TestingFunctions.md @@ -16,7 +16,7 @@ To import the testing library add the following dependency to your Maven project com.fnproject.fn testing - 1.0.0-SNAPSHOT + ${fdk.version} test ``` @@ -155,9 +155,33 @@ You can test that this is all handled correctly as follows: # Testing Fn Flows -You can use `FnTestingRule` to test [Fn Flows](FnFlowsUserGuide.md) within your functions. If flow stages are started by functions within `thenRun` then the testing rule will execute the stages of those flows locally, returning when all spawned flows are complete. +You can use `FlowTesting` to test [Fn Flows](FnFlowsUserGuide.md) within your functions. If flow stages are started by functions within `thenRun` then the testing rule will execute the stages of those flows locally, returning when all spawned flows are complete. -`FnTestingRule` supports mocking the behaviour of Fn functions invoked by the `invokeFunction()` API within flows. +Start by importing the `flow-testing` library into your functino in `test` scope: + +```xml + + com.fnproject.fn + flow-testing + ${fdk.version} + test + +``` + +Then create a `FlowTesting` field in your test class, passing the `FnTesting` rule as a parameter: + +```java +import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.flow.FlowTesting; + +public class FunctionTest { + @Rule + public final FnTestingRule testing = FnTestingRule.createDefault(); + + private final FlowTesting flowTesting = FlowTesting.create(testing); +``` + +`FlowTesting` supports mocking the behaviour of Fn functions invoked by the `invokeFunction()` API within flows. You can specify that the invocation a function returns a valid value (as a byte array): @@ -165,7 +189,7 @@ You can specify that the invocation a function returns a valid value (as a byte @Test public void callsRemoteFunctionWhichSucceeds() { - testing.givenFn("example/other-function").withResult("blah".getBytes()); + flowTesting.givenFn("example/other-function").withResult("blah".getBytes()); // ... @@ -178,8 +202,8 @@ Or you can specify that the invocation a function will cause a user error or a p @Test public void callsRemoteFunctionWhichCausesAnError() { - testing.givenFn("example/other-function").withFunctionError(); - testing.givenFn("example/other-function-2").withPlatformError(); + flowTesting.givenFn("example/other-function").withFunctionError(); + flowTesting.givenFn("example/other-function-2").withPlatformError(); // ... @@ -196,7 +220,7 @@ used to check some behavior: @Test public void callsRemoteFunction() { - testing.givenFn("example/other-function").withAction( (data) -> { called.set(true); return data; } ); + flowTesting.givenFn("example/other-function").withAction( (data) -> { called.set(true); return data; } ); called.set(false); @@ -221,7 +245,7 @@ If you need to share objects or static data between your test classes and your f ```java testing.addSharedClass(MyClassWithStaticState.class); // Shares only the specific class testing.addSharedPrefix("com.example.MyClassWithStaticState"); // Shares the class and anything under it - testing.addSharedPrefix("com.example.mysubpackage."); // Shares anyhting under a package + testing.addSharedPrefix("com.example.mysubpackage."); // Shares anything under a package ``` While it is possible, it is not generally correct to share the function class itself with the test Class Loader - doing so may result in unexpected (not representative of the real fn platform) initialisation of static fields on the class. With Flows sharing the test class may also result in concurrent access to static data (via `@FnConfiguration` methods). \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index 4600bd90..620fbee6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,36 +1,48 @@ -# `fn` Java FDK Example Projects +# Fn Java FDK Example Projects In this directory you will find some example projects demonstrating different -features of the `fn` Java FDK: +features of the Fn Java FDK: * Plain java code support (`string-reverse`) * Functional testing of your functions (`regex-query` and `qr-code`) * Built-in JSON coercion (`regex-query`) * [InputEvent and OutputEvent](/docs/DataBinding.md) handling (`qr-code`) -## 1. String reverse +## (1) [String reverse](string-reverse/README.md) This function takes a string and returns the reverse of the string. -The `fn` Java FDK runtime will handle marshalling data into your -functions without the function having to have any knowledge of the FDK API. +The Fn Java FDK handles marshalling data into your +functions without the function having any knowledge of the FDK API. -## 2. Regex query +## (2) Regex query This function takes a JSON object containing a `text` field and a `regex` field and return a JSON object with a list of matches in the `matches` field. It demonstrates the builtin JSON support of the fn Java -wrapper (provided through Jackson) and how the platform handles serialisation +wrapper (provided through Jackson) and how the platform handles serialization of POJO return values. -## 3. QR Code gen +## (3) QR Code gen This function parses the query parameters of a GET request (through the `InputEvent` passed into the function) to generate a QR code. It demonstrates the `InputEvent` and `OutputEvent` interfaces which provide low level access to data entering the `fn` Java FDK. -## 4. Asynchronous thumbnails generation +## (4) Asynchronous thumbnails generation This example showcases the Fn Flow asynchronous execution API, by creating a workflow that takes an image and asynchronously generates three thumbnails for it, then uploads them to an object storage. + +## (5) Gradle build +This shows how to use Gradle to build functions using the Java FDK. + +## (6) Fn Events +This shows how to use fn-events library for: +- OCI API Gateway Function - [README.md](../examples/apigateway-event/README.md) +- OCI Service Connector Hub: Monitoring - [README.md](../examples/connectorhub-monitoring/README.md) +- OCI Service Connector Hub: Logging - [README.md](../examples/connectorhub-logging/README.md) +- OCI Service Connector Hub: Streaming - [README.md](../examples/connectorhub-streaming/README.md) +- OCI Service Connector Hub: Queue - [connectorhub-queue](../examples/connectorhub-queue) +- OCI Notifications - [README.md](../examples/notifications/README.md) \ No newline at end of file diff --git a/examples/apigateway-event/README.md b/examples/apigateway-event/README.md new file mode 100644 index 00000000..0d3277e2 --- /dev/null +++ b/examples/apigateway-event/README.md @@ -0,0 +1,135 @@ +# Example Fn Java FDK : API Gateway + +This example provides a Function to use as an API Gateway backend. +The function accepts a typed request for easy object handling and returns +http response. + +## Dependencies + +* [fn-events] for APIGatewayFunction classes. +* [fn-events-testing] for APIGatewayFunction testing library. + +## Demonstrated FDK features + +This example showcases how to use the fn-event APIGatewayFunction to +use a Function as the backend. + +## Step by step + +Set the API Gateway and Function +backend [Adding a Function in OCI Functions as an API Gateway Back End](https://docs.oracle.com/en-us/iaas/Content/APIGateway/Tasks/apigatewayusingfunctionsbackend.htm) + +The Function entrypoint extends the `APIGatewayFunction` abstract class. +Note: the [func.yaml](func.yaml) entrypoint remains the class which extends `APIGatewayFunction` +e.g. `cmd: com.fnproject.fn.examples.Function::handler` + + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +```java +import com.fnproject.events.APIGatewayFunction; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.fn.api.Headers; +import org.apache.http.HttpStatus; + +public class Function extends APIGatewayFunction { + + @Override + public APIGatewayResponseEvent handler(APIGatewayRequestEvent requestEvent) { + ResponseEmployee employee = new ResponseEmployee(); + Optional id = requestEvent.getQueryParameters().get("id"); + id.ifPresent(s -> employee.setId(Integer.parseInt(s))); + + if (requestEvent.getBody() != null) { + employee.setName(requestEvent.getBody().getName()); + } + + return new APIGatewayResponseEvent.Builder() + .statusCode(HttpStatus.SC_CREATED) + .headers(Headers.emptyHeaders() + .addHeader("X-Custom-Header", "HeaderValue") + .addHeader("X-Custom-Header-2", "HeaderValue2")) + .body(employee) + .build(); + } +} +``` +The APIGatewayRequestEvent.class `requestUrl` is relative to the deployment +path prefix [see API Gateway using HTTP backend](https://docs.oracle.com/en-us/iaas/Content/APIGateway/Tasks/apigatewayusinghttpbackend.htm#usingjson) + + +The class [RequestEmployee.java](src/main/java/com/fnproject/fn/examples/RequestEmployee.java) is the request body type and +[ResponseEmployee.java](src/main/java/com/fnproject/fn/examples/ResponseEmployee.java) is the response body type. +These are passed in position 1 and 2 of abstract class +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +`public class Function extends APIGatewayFunction {`. + +To return an error response, throw RuntimeException.class. + +## Test walkthrough + +Unit testing `APIGatewayFunction` is supported with the `APIGatewayTestFeature` and `FnTestingRule`. + +First of all, the class initializes the `FnTestingRule` harness, as explained +in [Testing Functions](../../docs/TestingFunctions.md). + +[FunctionTest.java](src/test/java/com/fnproject/fn/examples/FunctionTest.java) +```java +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.events.testing.APIGatewayTestFeature; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final APIGatewayTestFeature apiGatewayFeature = APIGatewayTestFeature.createDefault(fn); + + @Test + public void testHttpAttributes() throws IOException { + RequestEmployee requestEmployee = new RequestEmployee(); + requestEmployee.setName("John"); + + APIGatewayRequestEvent event = mock(APIGatewayRequestEvent.class); + + when(event.getBody()).thenReturn(requestEmployee); + when(event.getMethod()).thenReturn("POST"); + when(event.getRequestUrl()).thenReturn("/v2?id=123"); + when(event.getQueryParameters()).thenReturn(new QueryParametersImpl(Collections.singletonMap("id", Collections.singletonList("123")))); + when(event.getHeaders()).thenReturn(Collections.unmodifiableMap(new HashMap>() {{ + put("myHeader", Collections.singletonList("headerValue")); + }})); + + apiGatewayFeature.givenEvent(event) + .enqueue(); + + fn.thenRun(Function.class, "handler"); + + APIGatewayResponseEvent responseEvent = apiGatewayFeature.getResult(ResponseEmployee.class); + + ResponseEmployee responseEventBody = responseEvent.getBody(); + assertEquals(123, responseEventBody.getId()); + assertEquals("John", responseEventBody.getName()); + assertEquals(Integer.valueOf(201), responseEvent.getStatus()); + assertEquals("HeaderValue", responseEvent.getHeaders().get("X-Custom-Header").get(0)); + assertEquals("HeaderValue2", responseEvent.getHeaders().get("X-Custom-Header-2").get(0)); + } +} +``` + +Use `apiGatewayFeature.givenEvent(event).enqueue();` to queue the request event +and invoke the Function with `fn.thenRun(Function.class, "handler");`. + +And get the Function response using +`APIGatewayResponseEvent responseEvent = apiGatewayFeature.getResult(ResponseEmployee.class);` diff --git a/examples/apigateway-event/func.yaml b/examples/apigateway-event/func.yaml new file mode 100644 index 00000000..1f72ed08 --- /dev/null +++ b/examples/apigateway-event/func.yaml @@ -0,0 +1,5 @@ +schema_version: 20180708 +name: apigateway-event +version: 0.0.1 +runtime: java +cmd: com.fnproject.fn.examples.Function::handler diff --git a/examples/apigateway-event/pom.xml b/examples/apigateway-event/pom.xml new file mode 100644 index 00000000..e1b8de57 --- /dev/null +++ b/examples/apigateway-event/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + + UTF-8 + UTF-8 + 1.0.0-SNAPSHOT + 2.16.1 + 4.0.0 + + + com.fnproject.fn.examples + apigateway-event + 1.0.0-SNAPSHOT + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + fn-events + ${fdk.version} + + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + diff --git a/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/EmployeeService.java b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/EmployeeService.java new file mode 100644 index 00000000..8b9357cb --- /dev/null +++ b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/EmployeeService.java @@ -0,0 +1,19 @@ +package com.fnproject.fn.examples; + +import java.util.Optional; + +public class EmployeeService { + + public ResponseEmployee createEmployee(RequestEmployee requestEmployee, Optional id) { + if (requestEmployee == null) { + throw new IllegalArgumentException("requestEmployee must not be null"); + } + if (!id.isPresent()) { + throw new IllegalArgumentException("id must not be null"); + } + ResponseEmployee employee = new ResponseEmployee(); + employee.setId(Integer.parseInt(id.get())); + employee.setName(requestEmployee.getName()); + return employee; + } +} diff --git a/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/Function.java b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/Function.java new file mode 100644 index 00000000..bf276d34 --- /dev/null +++ b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/Function.java @@ -0,0 +1,34 @@ +package com.fnproject.fn.examples; + +import java.util.Optional; +import com.fnproject.events.APIGatewayFunction; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.fn.api.Headers; +import org.apache.http.HttpStatus; + + +public class Function extends APIGatewayFunction { + + private final EmployeeService employeeService; + + public Function() { + this.employeeService = new EmployeeService(); + } + + @Override + public APIGatewayResponseEvent handler(APIGatewayRequestEvent requestEvent) { + Optional id = requestEvent.getQueryParameters().get("id"); + RequestEmployee requestEmployee = requestEvent.getBody(); + + ResponseEmployee responseEmployee = employeeService.createEmployee(requestEmployee, id); + + return new APIGatewayResponseEvent.Builder() + .statusCode(HttpStatus.SC_CREATED) + .headers(Headers.emptyHeaders() + .addHeader("X-Custom-Header", "HeaderValue") + .addHeader("X-Custom-Header-2", "HeaderValue2")) + .body(responseEmployee) + .build(); + } +} diff --git a/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/RequestEmployee.java b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/RequestEmployee.java new file mode 100644 index 00000000..7e1cc74c --- /dev/null +++ b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/RequestEmployee.java @@ -0,0 +1,16 @@ +package com.fnproject.fn.examples; + +public class RequestEmployee { + private String name; + + public RequestEmployee() {} + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/ResponseEmployee.java b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/ResponseEmployee.java new file mode 100644 index 00000000..9d056c57 --- /dev/null +++ b/examples/apigateway-event/src/main/java/com/fnproject/fn/examples/ResponseEmployee.java @@ -0,0 +1,40 @@ +package com.fnproject.fn.examples; + +import java.util.Objects; + +public class ResponseEmployee { + private Integer id; + private String name; + + public ResponseEmployee() {} + + public Integer getId() { + return this.id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResponseEmployee employee = (ResponseEmployee) o; + return id.equals(employee.id) && + Objects.equals(name, employee.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } +} \ No newline at end of file diff --git a/examples/apigateway-event/src/test/java/com/fnproject/fn/examples/FunctionTest.java b/examples/apigateway-event/src/test/java/com/fnproject/fn/examples/FunctionTest.java new file mode 100644 index 00000000..417ec880 --- /dev/null +++ b/examples/apigateway-event/src/test/java/com/fnproject/fn/examples/FunctionTest.java @@ -0,0 +1,96 @@ +package com.fnproject.fn.examples; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.IOException; +import java.util.Collections; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.events.testing.APIGatewayTestFeature; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.runtime.httpgateway.QueryParametersImpl; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final APIGatewayTestFeature apiGatewayFeature = APIGatewayTestFeature.createDefault(fn); + + @Test + public void testGetResponseBody() throws IOException { + APIGatewayRequestEvent event = createMinimalRequest(); + + apiGatewayFeature.givenEvent(event) + .enqueue(); + + fn.thenRun(Function.class, "handler"); + + APIGatewayResponseEvent responseEvent = apiGatewayFeature.getResult(ResponseEmployee.class); + + ResponseEmployee responseEventBody = responseEvent.getBody(); + assertEquals(Integer.valueOf(123), responseEventBody.getId()); + assertEquals("John", responseEventBody.getName()); + } + + @Test + public void testGetResponseHeaders() throws IOException { + APIGatewayRequestEvent event = createMinimalRequest(); + + when(event.getHeaders()).thenReturn(Headers.emptyHeaders().addHeader("myHeader", "headerValue")); + + apiGatewayFeature.givenEvent(event) + .enqueue(); + + fn.thenRun(Function.class, "handler"); + + APIGatewayResponseEvent responseEvent = apiGatewayFeature.getResult(ResponseEmployee.class); + assertEquals("HeaderValue", responseEvent.getHeaders().getAllValues("X-Custom-Header").get(0)); + assertEquals("HeaderValue2", responseEvent.getHeaders().get("X-Custom-Header-2").get()); + } + + @Test + public void testGetResponseStatus() throws IOException { + APIGatewayRequestEvent event = createMinimalRequest(); + + apiGatewayFeature.givenEvent(event) + .enqueue(); + + fn.thenRun(Function.class, "handler"); + + APIGatewayResponseEvent responseEvent = apiGatewayFeature.getResult(ResponseEmployee.class); + + assertEquals(Integer.valueOf(201), responseEvent.getStatus()); + } + + @Test + public void testErrorResponse() throws IOException { + APIGatewayRequestEvent event = mock(APIGatewayRequestEvent.class); + + apiGatewayFeature.givenEvent(event) + .enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(502, result.getStatus().getCode()); + assertEquals("An error occurred in function: requestEmployee must not be null\n" + + "Caused by: java.lang.IllegalArgumentException: requestEmployee must not be null\n\n", fn.getStdErrAsString()); + assertEquals(1, fn.getLastExitCode()); + } + + private static APIGatewayRequestEvent createMinimalRequest() { + RequestEmployee requestEmployee = new RequestEmployee(); + requestEmployee.setName("John"); + APIGatewayRequestEvent event = mock(APIGatewayRequestEvent.class); + + when(event.getBody()).thenReturn(requestEmployee); + when(event.getQueryParameters()).thenReturn(new QueryParametersImpl(Collections.singletonMap("id", Collections.singletonList("123")))); + return event; + } +} \ No newline at end of file diff --git a/examples/async-thumbnails/README.md b/examples/async-thumbnails/README.md index 10616498..b3cff27d 100644 --- a/examples/async-thumbnails/README.md +++ b/examples/async-thumbnails/README.md @@ -32,9 +32,8 @@ this example. Run: ``` This will start a local functions service, a local flow completion -service, and will set up a `myapp` application and three routes: `/resize128`, -`/resize256` and `/resize512`. The routes are implemented as Fn functions -which just invoke `imagemagick` to convert the images to the specified sizes. +service, and will set up a `myapp` application and three functions: `resize128`, +`resize256` and `resize512`. These functions just invoke `imagemagick` to convert the images to the specified sizes. The setup script also starts a docker container with an object storage daemon based on `minio` (with access key `alpha` and secret key `betabetabetabeta`). @@ -48,14 +47,9 @@ docker container, so that you can verify when the thumbnails are uploaded. Build the function locally: ```bash -$ fn build +$ fn deploy --local --app myapp ``` -Create a route to host the function: - -```bash -$ fn routes create myapp /async-thumbnails -``` Configure the app. In order to do this you must determine the IP address of the storage server docker container: @@ -68,18 +62,18 @@ $ docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAd and then use it as the storage host: ```bash -$ fn routes config set myapp /async-thumbnails OBJECT_STORAGE_URL http://172.17.0.4:9000 +$ fn config app myapp OBJECT_STORAGE_URL http://172.17.0.4:9000 myapp /async-thumbnails updated OBJECT_STORAGE_URL with http://172.17.0.4:9000 -$ fn routes config set myapp /async-thumbnails OBJECT_STORAGE_ACCESS alpha +$ fn config app myapp OBJECT_STORAGE_ACCESS alpha myapp /async-thumbnails updated OBJECT_STORAGE_ACCESS with alpha -$ fn routes config set myapp /async-thumbnails OBJECT_STORAGE_SECRET betabetabetabeta +$ fn config app myapp OBJECT_STORAGE_SECRET betabetabetabeta myapp /async-thumbnails updated OBJECT_STORAGE_SECRET with betabetabetabeta ``` Invoke the function by passing the provided test image: ```bash -$ curl -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/r/myapp/async-thumbnails" +$ curl -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/t/myapp/async-thumbnails" {"imageId":"bd74fff4-0388-4c6f-82f2-8cde9ba9b6fc"} ``` @@ -116,6 +110,13 @@ public class ThumbnailsFunction { .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_ACCESS")); storageSecretKey = ctx.getConfigurationByKey("OBJECT_STORAGE_SECRET") .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_SECRET")); + + resize128ID = ctx.getConfigurationByKey("RESIZE_128_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_128_FN_ID")); + resize256ID = ctx.getConfigurationByKey("RESIZE_256_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_256_FN_ID")); + resize512ID = ctx.getConfigurationByKey("RESIZE_512_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_512_FN_ID")); } // ... @@ -155,11 +156,11 @@ public class ThumbnailsFunction { Flow runtime = Flows.currentFlow(); runtime.allOf( - runtime.invokeFunction("myapp/resize128", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize128ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-128.png")), - runtime.invokeFunction("myapp/resize256", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize256ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-256.png")), - runtime.invokeFunction("myapp/resize512", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize512ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-512.png")), runtime.supply(() -> objectUpload(imageBuffer, id + ".png")) ); @@ -218,8 +219,9 @@ in [Testing Functions](../../docs/TestingFunctions.md). ```java public class ThumbnailsFunctionTest { - @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); + @Rule + public final FnTestingRule fn = FnTestingRule.createDefault(); + private final FlowTesting flow = FlowTesting.create(fn); // ... } @@ -259,20 +261,22 @@ public class ThumbnailsFunctionTest { @Test public void testThumbnail() { - testing - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + fn.setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512"); - .givenFn("myapp/resize128") + flow.givenFn("myapp/resize128") .withAction((data) -> "128".getBytes()) .givenFn("myapp/resize256") .withAction((data) -> "256".getBytes()) .givenFn("myapp/resize512") .withAction((data) -> "512".getBytes()) - .givenEvent() + fn.givenEvent() .withBody("testing".getBytes()) .enqueue(); @@ -301,21 +305,23 @@ public class ThumbnailsFunctionTest { @Test public void anExternalFunctionFailure() { - testing - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") - - .givenFn("myapp/resize128") - .withResult("128".getBytes()) - .givenFn("myapp/resize256") - .withResult("256".getBytes()) - .givenFn("myapp/resize512") - .withFunctionError() - - .givenEvent() - .withBody("testing".getBytes()) - .enqueue(); + fn.setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512"); + + flow.givenFn("myapp/resize128") + .withResult("128".getBytes()) + .givenFn("myapp/resize256") + .withResult("256".getBytes()) + .givenFn("myapp/resize512") + .withFunctionError(); + + fn.givenEvent() + .withBody("testing".getBytes()) + .enqueue(); // Mock the http endpoint mockMinio(); diff --git a/examples/async-thumbnails/func.yaml b/examples/async-thumbnails/func.yaml index d6003dc4..1f1f0d05 100644 --- a/examples/async-thumbnails/func.yaml +++ b/examples/async-thumbnails/func.yaml @@ -1,7 +1,27 @@ -name: fn-example/async-thumbnails -version: 0.0.1 +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +schema_version: 20180708 +name: async-thumbnails +version: 0.0.8 runtime: java cmd: com.fnproject.fn.examples.ThumbnailsFunction::handleRequest -path: /async-thumbnails -format: http -timeout: 30 +format: http-stream +timeout: 120 +triggers: +- name: async-thumbnails + type: http + source: /async-thumbnails diff --git a/examples/async-thumbnails/pom.xml b/examples/async-thumbnails/pom.xml index e554bee8..0a28f5fb 100644 --- a/examples/async-thumbnails/pom.xml +++ b/examples/async-thumbnails/pom.xml @@ -1,4 +1,22 @@ + + @@ -6,8 +24,11 @@ UTF-8 - 1.0.0-SNAPSHOT - 2.8.47 + UTF-8 + + 1.0.0-SNAPSHOT + 3.3.3 + 2.16.1 com.fnproject.fn.examples @@ -15,38 +36,137 @@ 1.0.0-SNAPSHOT + com.fnproject.fn api - ${fnproject.version} + ${fdk.version} + + + com.fnproject.fn + flow-runtime + ${fdk.version} commons-net commons-net - 3.6 + 3.8.0 + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} io.minio minio - 3.0.5 + 7.1.3 + + + com.squareup.okhttp3 + okhttp + + + + + com.squareup.okhttp3 + okhttp + 4.9.3 + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + flow-testing + ${fdk.version} + test com.fnproject.fn - testing - ${fnproject.version} + testing-junit4 + ${fdk.version} test junit junit - 4.12 + 4.13.2 test com.github.tomakehurst wiremock - 2.7.1 + 2.27.2 + + + com.jayway.jsonpath + json-path + + + com.github.jknack + handlebars + + + org.apache.httpcomponents + httpclient + + + + + + org.eclipse.jetty + jetty-server + 9.4.44.v20210927 + + + org.eclipse.jetty + jetty-servlet + 9.4.44.v20210927 + + + org.eclipse.jetty + jetty-servlets + 9.4.44.v20210927 + + + org.eclipse.jetty + jetty-webapp + 9.4.44.v20210927 + + + + com.jayway.jsonpath + json-path + 2.6.0 + + + + com.github.jknack + handlebars + 4.2.1 + + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + commons-codec + commons-codec + + + + + + commons-codec + commons-codec + 1.15 @@ -55,7 +175,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.3 + 3.8.0 1.8 1.8 @@ -71,11 +191,4 @@ - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - diff --git a/examples/async-thumbnails/run.sh b/examples/async-thumbnails/run.sh index 76315c0f..9744fd5c 100755 --- a/examples/async-thumbnails/run.sh +++ b/examples/async-thumbnails/run.sh @@ -1,15 +1,27 @@ #!/bin/bash +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# -fn build +set -e -fn routes create myapp /async-thumbnails +fn --verbose deploy --app myapp --local -STORAGE_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' example-storage-server` -fn routes config set myapp /async-thumbnails OBJECT_STORAGE_URL http://${STORAGE_SERVER_IP}:9000 -fn routes config set myapp /async-thumbnails OBJECT_STORAGE_ACCESS alpha -fn routes config set myapp /async-thumbnails OBJECT_STORAGE_SECRET betabetabetabeta -curl -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/r/myapp/async-thumbnails" +echo "Calling function" +curl -v -X POST --data-binary @test-image.png -H "Content-type: application/octet-stream" "http://localhost:8080/t/myapp/async-thumbnails" echo "Contents of bucket" mc ls -r example-storage-server diff --git a/examples/async-thumbnails/setup/resize128/Dockerfile b/examples/async-thumbnails/setup/resize128/Dockerfile index 20f55eec..65442048 100644 --- a/examples/async-thumbnails/setup/resize128/Dockerfile +++ b/examples/async-thumbnails/setup/resize128/Dockerfile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + FROM debian RUN apt-get update RUN apt-get install -y imagemagick diff --git a/examples/async-thumbnails/setup/resize128/func.yaml b/examples/async-thumbnails/setup/resize128/func.yaml index b7a5071b..4f6ea681 100644 --- a/examples/async-thumbnails/setup/resize128/func.yaml +++ b/examples/async-thumbnails/setup/resize128/func.yaml @@ -1,4 +1,21 @@ -name: example/resize128 -version: 0.0.1 +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +schema_version: 20180708 +name: resize128 +version: 0.0.5 entrypoint: convert - -resize 128x128 - -path: /resize128 +format: default diff --git a/examples/async-thumbnails/setup/resize256/Dockerfile b/examples/async-thumbnails/setup/resize256/Dockerfile index 433e17e1..4cb095ff 100644 --- a/examples/async-thumbnails/setup/resize256/Dockerfile +++ b/examples/async-thumbnails/setup/resize256/Dockerfile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + FROM debian RUN apt-get update RUN apt-get install -y imagemagick diff --git a/examples/async-thumbnails/setup/resize256/func.yaml b/examples/async-thumbnails/setup/resize256/func.yaml index 9261f2f6..2e655c82 100644 --- a/examples/async-thumbnails/setup/resize256/func.yaml +++ b/examples/async-thumbnails/setup/resize256/func.yaml @@ -1,4 +1,21 @@ -name: example/resize256 -version: 0.0.1 +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +schema_version: 20180708 +name: resize256 +version: 0.0.5 entrypoint: convert - -resize 256x256 - -path: /resize256 +format: default diff --git a/examples/async-thumbnails/setup/resize512/Dockerfile b/examples/async-thumbnails/setup/resize512/Dockerfile index 52a2b407..56469980 100644 --- a/examples/async-thumbnails/setup/resize512/Dockerfile +++ b/examples/async-thumbnails/setup/resize512/Dockerfile @@ -1,3 +1,19 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + FROM debian RUN apt-get update RUN apt-get install -y imagemagick diff --git a/examples/async-thumbnails/setup/resize512/func.yaml b/examples/async-thumbnails/setup/resize512/func.yaml index 8ee1d02f..bf9712ab 100644 --- a/examples/async-thumbnails/setup/resize512/func.yaml +++ b/examples/async-thumbnails/setup/resize512/func.yaml @@ -1,4 +1,21 @@ -name: example/resize512 -version: 0.0.1 +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +schema_version: 20180708 +name: resize512 +version: 0.0.8 entrypoint: convert - -resize 512x512 - -path: /resize512 +format: default diff --git a/examples/async-thumbnails/setup/setup.sh b/examples/async-thumbnails/setup/setup.sh index 501c0e60..6902145e 100755 --- a/examples/async-thumbnails/setup/setup.sh +++ b/examples/async-thumbnails/setup/setup.sh @@ -1,4 +1,20 @@ #!/bin/bash +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + set -e SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" STORAGE_DIR="/tmp/example-storage-server-files" @@ -51,26 +67,21 @@ fi STORAGE_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' example-storage-server` # Start functions server if not there -if [[ -z `docker ps | grep "functions"` ]]; then - docker run -d --name functions \ - -e NO_PROXY="$STORAGE_SERVER_IP:$NO_PROXY" \ - -p 8080:8080 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - "$FUNCTIONS_IMAGE" - # Give it time to start up +if [[ -z `docker ps | grep "fnserver"` ]]; then + fn start -d sleep 3 else echo "Functions server is already up." fi # Get its IP -FUNCTIONS_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' functions` +FUNCTIONS_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' fnserver` # Start flow service if not there if [[ -z `docker ps | grep "flow-service"` ]]; then docker run -d --name flow-service \ -e LOG_LEVEL=debug \ -e NO_PROXY="$FUNCTIONS_SERVER_IP:$NO_PROXY" \ - -e API_URL=http://$FUNCTIONS_SERVER_IP:8080/r \ + -e API_URL=http://$FUNCTIONS_SERVER_IP:8080/invoke \ -p 8081:8081 \ "$COMPLETER_IMAGE" # Give it time to start up @@ -82,46 +93,41 @@ fi COMPLETER_SERVER_IP=`docker inspect --type container -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' flow-service` # Create app and routes -if [[ `fn apps list` == *"myapp"* ]]; then +if [[ `fn list apps` == *"myapp"* ]]; then echo "App myapp is already there." else - fn apps create myapp - fn apps config set myapp COMPLETER_BASE_URL http://10.167.103.193:8081 + fn create app myapp fi -if [[ `fn routes list myapp` == *"/resize128"* ]]; then - echo "Route /resize128 is already there." -else - # This works around proxy issues - cd $SCRIPT_DIR/resize128 && \ - docker build -t example/resize128:0.0.1 \ - --build-arg http_proxy=$http_proxy \ - --build-arg https_proxy=$https_proxy \ - . && \ - fn routes create myapp /resize128 -fi -if [[ `fn routes list myapp` == *"/resize256"* ]]; then - echo "Route /resize256 is already there." -else - # This works around proxy issues - cd $SCRIPT_DIR/resize256 && \ - docker build -t example/resize256:0.0.1 \ - --build-arg http_proxy=$http_proxy \ - --build-arg https_proxy=$https_proxy \ - . && \ - fn routes create myapp /resize256 -fi -if [[ `fn routes list myapp` == *"/resize512"* ]]; then - echo "Route /resize512 is already there." -else - # This works around proxy issues - cd $SCRIPT_DIR/resize512 && \ - docker build -t example/resize512:0.0.1 \ - --build-arg http_proxy=$http_proxy \ - --build-arg https_proxy=$https_proxy \ - . && \ - fn routes create myapp /resize512 -fi + +fn config app myapp COMPLETER_BASE_URL http://${COMPLETER_SERVER_IP}:8081 +fn config app myapp OBJECT_STORAGE_URL http://${STORAGE_SERVER_IP}:9000 +fn config app myapp OBJECT_STORAGE_ACCESS alpha +fn config app myapp OBJECT_STORAGE_SECRET betabetabetabeta + +( + cd ${SCRIPT_DIR}/resize128 + fn deploy --app myapp --local +) + +fn config app myapp RESIZE_128_FN_ID $(fn list functions myapp | grep resize128 | awk '{print $3}') + +( + cd ${SCRIPT_DIR}/resize256 + fn deploy --app myapp --local +) + +fn config app myapp RESIZE_256_FN_ID $(fn list functions myapp | grep resize256 | awk '{print $3}') + + +( + cd ${SCRIPT_DIR}/resize512 + fn deploy --app myapp --local +) + +fn config app myapp RESIZE_512_FN_ID $(fn list functions myapp | grep resize512 | awk '{print $3}') + + if mc config host list | grep example-storage-server &>/dev/null ; then diff --git a/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java b/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java index 7ad601de..04b853a8 100644 --- a/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java +++ b/examples/async-thumbnails/src/main/java/com/fnproject/fn/examples/ThumbnailsFunction.java @@ -1,21 +1,47 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.Headers; import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.flow.Flow; +import com.fnproject.fn.runtime.flow.FlowFeature; import com.fnproject.fn.api.flow.Flows; import com.fnproject.fn.api.flow.HttpMethod; +import io.minio.BucketExistsArgs; +import io.minio.MakeBucketArgs; import io.minio.MinioClient; +import io.minio.PutObjectArgs; import java.io.ByteArrayInputStream; import java.io.Serializable; +@FnFeature(FlowFeature.class) public class ThumbnailsFunction implements Serializable { private final String storageUrl; private final String storageAccessKey; private final String storageSecretKey; + private final String resize128ID; + private final String resize256ID; + private final String resize512ID; + public ThumbnailsFunction(RuntimeContext ctx) { storageUrl = ctx.getConfigurationByKey("OBJECT_STORAGE_URL") .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_URL")); @@ -23,6 +49,14 @@ public ThumbnailsFunction(RuntimeContext ctx) { .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_ACCESS")); storageSecretKey = ctx.getConfigurationByKey("OBJECT_STORAGE_SECRET") .orElseThrow(() -> new RuntimeException("Missing configuration: OBJECT_STORAGE_SECRET")); + + resize128ID = ctx.getConfigurationByKey("RESIZE_128_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_128_FN_ID")); + resize256ID = ctx.getConfigurationByKey("RESIZE_256_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_256_FN_ID")); + resize512ID = ctx.getConfigurationByKey("RESIZE_512_FN_ID") + .orElseThrow(() -> new RuntimeException("Missing configuration: RESIZE_512_FN_ID")); + } public class Response { @@ -35,11 +69,11 @@ public Response handleRequest(byte[] imageBuffer) { Flow runtime = Flows.currentFlow(); runtime.allOf( - runtime.invokeFunction("myapp/resize128", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize128ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-128.png")), - runtime.invokeFunction("myapp/resize256", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize256ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-256.png")), - runtime.invokeFunction("myapp/resize512", HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) + runtime.invokeFunction(resize512ID, HttpMethod.POST, Headers.emptyHeaders(), imageBuffer) .thenAccept((img) -> objectUpload(img.getBodyAsBytes(), id + "-512.png")), runtime.supply(() -> objectUpload(imageBuffer, id + ".png")) ); @@ -54,15 +88,22 @@ public Response handleRequest(byte[] imageBuffer) { */ private void objectUpload(byte[] imageBuffer, String objectName) { try { - MinioClient minioClient = new MinioClient(storageUrl, storageAccessKey, storageSecretKey); + MinioClient minioClient = MinioClient.builder() + .endpoint(storageUrl).credentials(storageAccessKey, storageSecretKey).build(); // Ensure the bucket exists. - if(!minioClient.bucketExists("alpha")) { - minioClient.makeBucket("alpha"); + BucketExistsArgs bucketExistsArgs = BucketExistsArgs.builder().bucket("alpha").build(); + MakeBucketArgs makeBucketArgs = MakeBucketArgs.builder().bucket("alpha").build(); + if(!minioClient.bucketExists(bucketExistsArgs)) { + minioClient.makeBucket(makeBucketArgs); } + PutObjectArgs putObjectArgs = PutObjectArgs.builder() + .bucket("alpha") + .object(objectName) + .stream(new ByteArrayInputStream(imageBuffer), imageBuffer.length, -1).build(); // Upload the image to the bucket with putObject - minioClient.putObject("alpha", objectName, new ByteArrayInputStream(imageBuffer), imageBuffer.length, "application/octet-stream"); + minioClient.putObject(putObjectArgs); } catch(Exception e) { System.err.println("Error occurred: " + e); e.printStackTrace(); diff --git a/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java b/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java index 675ca691..8914acc5 100644 --- a/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java +++ b/examples/async-thumbnails/src/test/java/com/fnproject/fn/examples/ThumbnailsFunctionTest.java @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; -import com.fnproject.fn.examples.ThumbnailsFunction; import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.flow.FlowTesting; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.junit.Rule; @@ -12,65 +28,76 @@ public class ThumbnailsFunctionTest { @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); + public final FnTestingRule fn = FnTestingRule.createDefault(); + private final FlowTesting flow = FlowTesting.create(fn); @Rule public final WireMockRule mockServer = new WireMockRule(0); @Test public void testThumbnail() { - testing - - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") - - .givenFn("myapp/resize128") - .withAction((data) -> "128".getBytes()) - .givenFn("myapp/resize256") - .withAction((data) -> "256".getBytes()) - .givenFn("myapp/resize512") - .withAction((data) -> "512".getBytes()) - - .givenEvent() - .withBody("testing".getBytes()) - .enqueue(); + fn + .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512"); + + + flow + .givenFn("myapp/resize128") + .withAction((data) -> "128".getBytes()) + .givenFn("myapp/resize256") + .withAction((data) -> "256".getBytes()) + .givenFn("myapp/resize512") + .withAction((data) -> "512".getBytes()); + + fn + .givenEvent() + .withBody("fn".getBytes()) + .enqueue(); // Mock the http endpoint mockMinio(); - testing.thenRun(ThumbnailsFunction.class, "handleRequest"); + fn.thenRun(ThumbnailsFunction.class, "handleRequest"); // Check the final image uploads were performed - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("testing"))); - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("128"))); - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("256"))); - mockServer.verify(1, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("512"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("fn"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("128"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("256"))); + mockServer.verify(putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(containing("512"))); mockServer.verify(4, putRequestedFor(urlMatching(".*"))); } @Test public void anExternalFunctionFailure() { - testing - .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) - .setConfig("OBJECT_STORAGE_ACCESS", "alpha") - .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") - - .givenFn("myapp/resize128") - .withResult("128".getBytes()) - .givenFn("myapp/resize256") - .withResult("256".getBytes()) - .givenFn("myapp/resize512") - .withFunctionError() - - .givenEvent() - .withBody("testing".getBytes()) - .enqueue(); + fn + .setConfig("OBJECT_STORAGE_URL", "http://localhost:" + mockServer.port()) + .setConfig("OBJECT_STORAGE_ACCESS", "alpha") + .setConfig("OBJECT_STORAGE_SECRET", "betabetabetabeta") + .setConfig("RESIZE_128_FN_ID","myapp/resize128") + .setConfig("RESIZE_256_FN_ID","myapp/resize256") + .setConfig("RESIZE_512_FN_ID","myapp/resize512");; + + flow + .givenFn("myapp/resize128") + .withResult("128".getBytes()) + .givenFn("myapp/resize256") + .withResult("256".getBytes()) + .givenFn("myapp/resize512") + .withFunctionError(); + + fn + .givenEvent() + .withBody("fn".getBytes()) + .enqueue(); // Mock the http endpoint mockMinio(); - testing.thenRun(ThumbnailsFunction.class, "handleRequest"); + fn.thenRun(ThumbnailsFunction.class, "handleRequest"); // Confirm that one image upload didn't happen mockServer.verify(0, putRequestedFor(urlMatching("/alpha/.*\\.png")).withRequestBody(equalTo("512"))); @@ -82,15 +109,15 @@ public void anExternalFunctionFailure() { private void mockMinio() { mockServer.stubFor(get(urlMatching("/alpha.*")) - .willReturn(aResponse().withBody( - "\n" + - "\n" + - " alpha\n" + - " \n" + - " 0\n" + - " 100\n" + - " false\n" + - ""))); + .willReturn(aResponse().withBody( + "\n" + + "\n" + + " alpha\n" + + " \n" + + " 0\n" + + " 100\n" + + " false\n" + + ""))); mockServer.stubFor(WireMock.head(urlMatching("/alpha.*")).willReturn(aResponse().withStatus(200))); diff --git a/examples/connectorhub-logging/README.md b/examples/connectorhub-logging/README.md new file mode 100644 index 00000000..992474e2 --- /dev/null +++ b/examples/connectorhub-logging/README.md @@ -0,0 +1,93 @@ +# Example Fn Java FDK : Service Connector Hub - Logging + +This example provides a Function to use as a service connector hub target. +The function accepts a typed event containing a batch of source events. + +## Source +[LoggingData.java](../../fn-events/src/main/java/com/fnproject/events/input/sch/LoggingData.java) + +## Dependencies +* [fn-events] for ConnectorHubFunction classes. +* [fn-events-testing] for ConnectorHubFunction testing library. + +## Demonstrated FDK features + +This example showcases how to use the fn-event ConnectorHubFunction to +use a Function as the target for Logging source. + +## Step by step + +Set up the connector hub with Logging source and Function target: +* [Setup default policies](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#Authenti__default-policies) +* [create connector hub](https://docs.oracle.com/en-us/iaas/Content/connector-hub/create-service-connector-logging-source.htm) + +The Function entrypoint extends the `ConnectorHubFunction` abstract class. +Note: the [func.yaml](func.yaml) entrypoint remains the class which extends `ConnectorHubFunction` +e.g. `cmd: com.fnproject.fn.examples.Function::handler` + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +```java +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.LoggingData; + +public class Function extends ConnectorHubFunction { + + public LogService logService; + + public Function() { + this.logService = new LogService(); + } + + @Override + public void handler(ConnectorHubBatch batch) { + for (LoggingData log : batch.getBatch()) { + logService.readLog(log); + } + } +} +``` +The [ConnectorHubBatch.java](../../fn-events/src/main/java/com/fnproject/events/input/ConnectorHubBatch.java) +`batch` contains a list of events from Logging as +specified in [Batch Settings](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#batch-settings). + +The class [LoggingData.java](../../fn-events/src/main/java/com/fnproject/events/input/sch/LoggingData.java) is +each logging event. + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +`public class Function extends ConnectorHubFunction`. + +To return an error response, throw RuntimeException.class. +Doing so will cause the Function to return a 502 [Retry policy](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#deactivate) + +## Test walkthrough + +Unit testing `ConnectorHubFunction` is supported with the `ConnectorHubTestFeature` and `FnTestingRule`. + +First of all, the class initializes the `FnTestingRule` harness, as explained +in [Testing Functions](../../docs/TestingFunctions.md). + +[FunctionTest.java](src/test/java/com/fnproject/fn/examples/FunctionTest.java) +```java +@Rule +public FnTestingRule fn = FnTestingRule.createDefault(); + +private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + +@Test +public void testInvokeFunctionWithLoggingData() throws Exception { + + ConnectorHubBatch event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); +} +``` + +Use `connectorHubTestFeature.givenEvent(event).enqueue();` to queue the request event +and invoke the Function with `fn.thenRun(Function.class, "handler");`. diff --git a/examples/connectorhub-logging/func.yaml b/examples/connectorhub-logging/func.yaml new file mode 100644 index 00000000..e5dface3 --- /dev/null +++ b/examples/connectorhub-logging/func.yaml @@ -0,0 +1,5 @@ +schema_version: 20180708 +name: connectorhub-logging +version: 0.0.1 +runtime: java +cmd: com.fnproject.fn.examples.Function::handler diff --git a/examples/connectorhub-logging/pom.xml b/examples/connectorhub-logging/pom.xml new file mode 100644 index 00000000..f768c714 --- /dev/null +++ b/examples/connectorhub-logging/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + + UTF-8 + UTF-8 + 1.0.0-SNAPSHOT + 2.16.1 + 4.0.0 + + + com.fnproject.fn.examples + connectorhub-logging + 1.0.0-SNAPSHOT + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + fn-events + ${fdk.version} + + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + diff --git a/examples/connectorhub-logging/src/main/java/com/fnproject/fn/examples/Function.java b/examples/connectorhub-logging/src/main/java/com/fnproject/fn/examples/Function.java new file mode 100644 index 00000000..9aa37965 --- /dev/null +++ b/examples/connectorhub-logging/src/main/java/com/fnproject/fn/examples/Function.java @@ -0,0 +1,21 @@ +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.LoggingData; + +public class Function extends ConnectorHubFunction { + + public LogService logService; + + public Function() { + this.logService = new LogService(); + } + + @Override + public void handler(ConnectorHubBatch batch) { + for (LoggingData log : batch.getBatch()) { + logService.readLog(log); + } + } +} \ No newline at end of file diff --git a/examples/connectorhub-logging/src/main/java/com/fnproject/fn/examples/LogService.java b/examples/connectorhub-logging/src/main/java/com/fnproject/fn/examples/LogService.java new file mode 100644 index 00000000..f0145b2c --- /dev/null +++ b/examples/connectorhub-logging/src/main/java/com/fnproject/fn/examples/LogService.java @@ -0,0 +1,19 @@ +package com.fnproject.fn.examples; + + +import com.fnproject.events.input.sch.LoggingData; + +public class LogService { + + public void readLog(LoggingData loggingData) { + System.out.println(loggingData); + assert loggingData != null; + assert loggingData.getData() != null && !loggingData.getData().isEmpty(); + assert loggingData.getId() != null; + assert loggingData.getOracle() != null && !loggingData.getOracle().isEmpty();; + assert loggingData.getSource() != null; + assert loggingData.getSpecversion() != null; + assert loggingData.getTime() != null; + assert loggingData.getType() != null; + } +} diff --git a/examples/connectorhub-logging/src/test/java/com/fnproject/fn/examples/FunctionTest.java b/examples/connectorhub-logging/src/test/java/com/fnproject/fn/examples/FunctionTest.java new file mode 100644 index 00000000..32ab2414 --- /dev/null +++ b/examples/connectorhub-logging/src/test/java/com/fnproject/fn/examples/FunctionTest.java @@ -0,0 +1,71 @@ +package com.fnproject.fn.examples; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.Datapoint; +import com.fnproject.events.input.sch.LoggingData; +import com.fnproject.events.input.sch.MetricData; +import com.fnproject.events.testing.ConnectorHubTestFeature; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithLoggingData() throws Exception { + + ConnectorHubBatch event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static ConnectorHubBatch createMinimalRequest() { + Map data = new HashMap(); + data.put("applicationId", "ocid1.fnapp.oc1.xyz"); + data.put("containerId", "n/a"); + data.put("functionId", "ocid1.fnfunc.oc1.xyz"); + data.put("message", "Received function invocation request"); + data.put("opcRequestId", "/abc/def"); + data.put("requestId", "/def/abc"); + data.put("src", "STDOUT"); + + Map oracle = new HashMap(); + oracle.put("compartmentid", "ocid1.tenancy.oc1.xyz"); + oracle.put("ingestedtime", "2025-10-23T15:45:19.457Z"); + oracle.put("loggroupid", "ocid1.loggroup.oc1.abc"); + oracle.put("logid", "ocid1.log.oc1.abc"); + oracle.put("tenantid", "ocid1.tenancy.oc1.xyz"); + + LoggingData source = new LoggingData( + "ecb37864-4396-4302-9575-981644949730", + "log-name", + "1.0", + "schedule", + "com.oraclecloud.functions.application.functioninvoke", + data, + oracle, + new Date(1764860467553L) + ); + ConnectorHubBatch event = mock(ConnectorHubBatch.class); + + when(event.getBatch()).thenReturn(Collections.singletonList(source)); + return event; + } +} \ No newline at end of file diff --git a/examples/connectorhub-monitoring/README.md b/examples/connectorhub-monitoring/README.md new file mode 100644 index 00000000..b3790435 --- /dev/null +++ b/examples/connectorhub-monitoring/README.md @@ -0,0 +1,95 @@ +# Example Fn Java FDK : Service Connector Hub - Monitoring + +This example provides a Function to use as a service connector hub target. +The function accepts a typed event containing a batch of source events. + +## Source +[MetricData](https://docs.oracle.com/en-us/iaas/Content/connector-hub/create-service-connector-monitoring-source.htm) + +## Dependencies +* [fn-events] for ConnectorHubFunction classes. +* [fn-events-testing] for ConnectorHubFunction testing library. + +## Demonstrated FDK features + +This example showcases how to use the fn-event ConnectorHubFunction to +use a Function as the target for Monitoring source. + +## Step by step + +Set up the connector hub with Monitoring source and Function target: +* [Setup default policies](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#Authenti__default-policies) +* [create connector hub](https://docs.oracle.com/en-us/iaas/Content/connector-hub/create-service-connector-monitoring-source.htm) + +The Function entrypoint extends the `ConnectorHubFunction` abstract class. +Note: the [func.yaml](func.yaml) entrypoint remains the class which extends `ConnectorHubFunction` +e.g. `cmd: com.fnproject.fn.examples.Function::handler` + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +```java +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.MetricData; + +public class Function extends ConnectorHubFunction { + + public MetricService metricService; + + public Function() { + this.metricService = new MetricService(); + } + + @Override + public void handler(ConnectorHubBatch batch) { + for (MetricData metric : batch.getBatch()) { + metricService.readMetric(metric); + } + } +} +``` +The [ConnectorHubBatch.java](../../fn-events/src/main/java/com/fnproject/events/input/ConnectorHubBatch.java) +`batch` contains a list of events from Monitoring as +specified in [Batch Settings](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#batch-settings). + +The class [MetricData.java](../../fn-events/src/main/java/com/fnproject/events/input/sch/MetricData.java) is +each Monitoring Event [Monitoring Schema](https://docs.oracle.com/en-us/iaas/Content/connector-hub/create-service-connector-monitoring-source.htm) + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +`public class Function extends ConnectorHubFunction {`. + +To return an error response, throw RuntimeException.class. +Doing so will cause the Function to return a 502 [Retry policy](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#deactivate) + +## Test walkthrough + +Unit testing `ConnectorHubFunction` is supported with the `ConnectorHubTestFeature` and `FnTestingRule`. + +First of all, the class initializes the `FnTestingRule` harness, as explained +in [Testing Functions](../../docs/TestingFunctions.md). + +[FunctionTest.java](src/test/java/com/fnproject/fn/examples/FunctionTest.java) +```java + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testMetricServiceConsumesEachMetric() throws Exception { + + ConnectorHubBatch event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + +``` + +Use `connectorHubTestFeature.givenEvent(event).enqueue();` to queue the request event +and invoke the Function with `fn.thenRun(Function.class, "handler");`. diff --git a/examples/connectorhub-monitoring/func.yaml b/examples/connectorhub-monitoring/func.yaml new file mode 100644 index 00000000..c7b5de2a --- /dev/null +++ b/examples/connectorhub-monitoring/func.yaml @@ -0,0 +1,5 @@ +schema_version: 20180708 +name: connectorhub-monitoring +version: 0.0.1 +runtime: java +cmd: com.fnproject.fn.examples.Function::handler diff --git a/examples/connectorhub-monitoring/pom.xml b/examples/connectorhub-monitoring/pom.xml new file mode 100644 index 00000000..b955ccfa --- /dev/null +++ b/examples/connectorhub-monitoring/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + + UTF-8 + UTF-8 + 1.0.0-SNAPSHOT + 2.16.1 + 4.0.0 + + + com.fnproject.fn.examples + connectorhub-monitoring + 1.0.0-SNAPSHOT + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + fn-events + ${fdk.version} + + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + diff --git a/examples/connectorhub-monitoring/src/main/java/com/fnproject/fn/examples/Function.java b/examples/connectorhub-monitoring/src/main/java/com/fnproject/fn/examples/Function.java new file mode 100644 index 00000000..37e86b4a --- /dev/null +++ b/examples/connectorhub-monitoring/src/main/java/com/fnproject/fn/examples/Function.java @@ -0,0 +1,21 @@ +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.MetricData; + +public class Function extends ConnectorHubFunction { + + public MetricService metricService; + + public Function() { + this.metricService = new MetricService(); + } + + @Override + public void handler(ConnectorHubBatch batch) { + for (MetricData metric : batch.getBatch()) { + metricService.readMetric(metric); + } + } +} \ No newline at end of file diff --git a/examples/connectorhub-monitoring/src/main/java/com/fnproject/fn/examples/MetricService.java b/examples/connectorhub-monitoring/src/main/java/com/fnproject/fn/examples/MetricService.java new file mode 100644 index 00000000..c1750638 --- /dev/null +++ b/examples/connectorhub-monitoring/src/main/java/com/fnproject/fn/examples/MetricService.java @@ -0,0 +1,18 @@ +package com.fnproject.fn.examples; + + +import com.fnproject.events.input.sch.MetricData; + +public class MetricService { + + public void readMetric(MetricData metric) { + System.out.println(metric); + assert metric != null; + assert metric.getDatapoints() != null && !metric.getDatapoints().isEmpty(); + assert metric.getCompartmentId() != null; + assert metric.getDimensions() != null && !metric.getDimensions().isEmpty(); + assert metric.getMetadata() != null && !metric.getMetadata().isEmpty(); + assert metric.getName() != null && !metric.getName().isEmpty(); + assert metric.getNamespace() != null; + } +} diff --git a/examples/connectorhub-monitoring/src/test/java/com/fnproject/fn/examples/FunctionTest.java b/examples/connectorhub-monitoring/src/test/java/com/fnproject/fn/examples/FunctionTest.java new file mode 100644 index 00000000..e056f74c --- /dev/null +++ b/examples/connectorhub-monitoring/src/test/java/com/fnproject/fn/examples/FunctionTest.java @@ -0,0 +1,60 @@ +package com.fnproject.fn.examples; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.Datapoint; +import com.fnproject.events.input.sch.MetricData; +import com.fnproject.events.testing.ConnectorHubTestFeature; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithMetricData() throws Exception { + + ConnectorHubBatch event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static ConnectorHubBatch createMinimalRequest() { + Map dimensions = new HashMap<>(); + dimensions.put("resourceID", "ocid1.bucket.oc1.xyz"); + dimensions.put("resourceDisplayName", "userName"); + Map metadata = new HashMap<>(); + metadata.put("displayName", "PutObject Request Count"); + metadata.put("unit", "count"); + + MetricData source = new MetricData( + "oci_objectstorage", + "unknown", + "ocid1.tenancy.oc1..xyz", + "PutRequests", + dimensions, + metadata, + Collections.singletonList(new Datapoint(new Date(1764860467553L), Double.parseDouble("12.3"), null)) + ); + ConnectorHubBatch event = mock(ConnectorHubBatch.class); + + when(event.getBatch()).thenReturn(Collections.singletonList(source)); + return event; + } +} \ No newline at end of file diff --git a/examples/connectorhub-queue/README.md b/examples/connectorhub-queue/README.md new file mode 100644 index 00000000..215eff2c --- /dev/null +++ b/examples/connectorhub-queue/README.md @@ -0,0 +1,124 @@ +# Example Fn Java FDK : Service Connector Hub - Queue + +This example provides a Function to use as a service connector hub target. +The function accepts a typed event containing a batch of source messages. + +## Source +[LoggingData.java](../../fn-events/src/main/java/com/fnproject/events/input/sch/LoggingData.java) + +## Dependencies +* [fn-events] for ConnectorHubFunction classes. +* [fn-events-testing] for ConnectorHubFunction testing library. + +## Demonstrated FDK features + +This example showcases how to use the fn-event ConnectorHubFunction to +use a Function as the target for Queue source. + +## Step by step + +Set up the connector hub with Logging source and Function target: +* [Setup default policies](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#Authenti__default-policies) +* [create connector hub](https://docs.oracle.com/en-us/iaas/Content/connector-hub/create-service-connector-queue-source.htm) + +The Function entrypoint extends the `ConnectorHubFunction` abstract class. +Note: the [func.yaml](func.yaml) entrypoint remains the class which extends `ConnectorHubFunction` +e.g. `cmd: com.fnproject.fn.examples.Function::handler` + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) + +```java +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; + +public class Function extends ConnectorHubFunction { + + public QueueService queueService; + + public Function() { + this.queueService = new QueueService(); + } + + @Override + public void handler(ConnectorHubBatch batch) { + for (Employee employee : batch.getBatch()) { + queueService.readContent(employee); + } + } +} +``` +The [ConnectorHubBatch.java](../../fn-events/src/main/java/com/fnproject/events/input/ConnectorHubBatch.java) +`batch` contains a list of messages from Queue as +specified in [Batch Settings](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#batch-settings). + +The class [Employee.java](src/main/java/com/fnproject/fn/examples/Employee.java) is +a representation of messages received from the Queue. + +Note the messages sent in to the Queue must be a valid String because Connector Hub forwards the message directly to +the Function target. +- wrong: a plain string +- correct: "a plain string" + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +`public class Function extends ConnectorHubFunction`. + +To return an error response, throw RuntimeException.class. +Doing so will cause the Function to return a 502 [Retry policy](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#deactivate) + +## Test walkthrough + +Unit testing `ConnectorHubFunction` is supported with the `ConnectorHubTestFeature` and `FnTestingRule`. + +First of all, the class initializes the `FnTestingRule` harness, as explained +in [Testing Functions](../../docs/TestingFunctions.md). + +[FunctionTest.java](src/test/java/com/fnproject/fn/examples/FunctionTest.java) +```java +package com.fnproject.fn.examples; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Collections; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.testing.ConnectorHubTestFeature; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithLoggingData() throws Exception { + + ConnectorHubBatch event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static ConnectorHubBatch createMinimalRequest() { + Employee employee = new Employee(); + employee.setName("foo"); + + ConnectorHubBatch event = mock(ConnectorHubBatch.class); + + when(event.getBatch()).thenReturn(Collections.singletonList(employee)); + return event; + } +} +``` + +Use `connectorHubTestFeature.givenEvent(event).enqueue();` to queue the request event +and invoke the Function with `fn.thenRun(Function.class, "handler");`. diff --git a/examples/connectorhub-queue/func.yaml b/examples/connectorhub-queue/func.yaml new file mode 100644 index 00000000..6cef20c6 --- /dev/null +++ b/examples/connectorhub-queue/func.yaml @@ -0,0 +1,5 @@ +schema_version: 20180708 +name: connectorhub-queue +version: 0.0.1 +runtime: java +cmd: com.fnproject.fn.examples.Function::handler diff --git a/examples/connectorhub-queue/pom.xml b/examples/connectorhub-queue/pom.xml new file mode 100644 index 00000000..7f29999c --- /dev/null +++ b/examples/connectorhub-queue/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + + UTF-8 + UTF-8 + 1.0.0-SNAPSHOT + 2.16.1 + 4.0.0 + + + com.fnproject.fn.examples + connectorhub-queue + 1.0.0-SNAPSHOT + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + fn-events + ${fdk.version} + + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + diff --git a/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/Employee.java b/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/Employee.java new file mode 100644 index 00000000..21079ea8 --- /dev/null +++ b/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/Employee.java @@ -0,0 +1,41 @@ +package com.fnproject.fn.examples; + +import java.util.Objects; + +public class Employee { + private String name; + + public Employee() {} + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Employee employee = (Employee) o; + return Objects.equals(name, employee.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + @Override + public String toString() { + return "Employee{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/Function.java b/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/Function.java new file mode 100644 index 00000000..79423d49 --- /dev/null +++ b/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/Function.java @@ -0,0 +1,20 @@ +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; + +public class Function extends ConnectorHubFunction { + + public QueueService queueService; + + public Function() { + this.queueService = new QueueService(); + } + + @Override + public void handler(ConnectorHubBatch batch) { + for (Employee employee : batch.getBatch()) { + queueService.readContent(employee); + } + } +} \ No newline at end of file diff --git a/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/QueueService.java b/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/QueueService.java new file mode 100644 index 00000000..748360aa --- /dev/null +++ b/examples/connectorhub-queue/src/main/java/com/fnproject/fn/examples/QueueService.java @@ -0,0 +1,10 @@ +package com.fnproject.fn.examples; + + +public class QueueService { + + public void readContent(Employee employee) { + System.out.println(employee); + assert employee != null; + } +} diff --git a/examples/connectorhub-queue/src/test/java/com/fnproject/fn/examples/FunctionTest.java b/examples/connectorhub-queue/src/test/java/com/fnproject/fn/examples/FunctionTest.java new file mode 100644 index 00000000..8f0324a5 --- /dev/null +++ b/examples/connectorhub-queue/src/test/java/com/fnproject/fn/examples/FunctionTest.java @@ -0,0 +1,42 @@ +package com.fnproject.fn.examples; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Collections; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.testing.ConnectorHubTestFeature; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithQueueData() throws Exception { + + ConnectorHubBatch event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static ConnectorHubBatch createMinimalRequest() { + Employee employee = new Employee(); + employee.setName("foo"); + + ConnectorHubBatch event = mock(ConnectorHubBatch.class); + + when(event.getBatch()).thenReturn(Collections.singletonList(employee)); + return event; + } +} \ No newline at end of file diff --git a/examples/connectorhub-streaming/README.md b/examples/connectorhub-streaming/README.md new file mode 100644 index 00000000..89361024 --- /dev/null +++ b/examples/connectorhub-streaming/README.md @@ -0,0 +1,133 @@ +# Example Fn Java FDK : Service Connector Hub - Streaming + +This example provides a Function to use as a service connector hub target. +The function accepts a typed event containing a batch of messages. + +## Source +[Streaming](https://docs.oracle.com/en-us/iaas/api/#/en/streaming/20180418/Message) +The value in each streaming event is delivered as base64 encoded. The library automatically decodes it. + +## Dependencies +* [fn-events] for ConnectorHubFunction classes. +* [fn-events-testing] for ConnectorHubFunction testing library. + +## Demonstrated FDK features + +This example showcases how to use the fn-event ConnectorHubFunction to +use a Function as the target for Streaming source. + +## Step by step + +Set up the connector hub with Streaming source and Function target: +* [Setup default policies](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#Authenti__default-policies) +* [create connector hub](https://docs.oracle.com/en-us/iaas/Content/connector-hub/create-service-connector-streaming-source.htm) + +The Function entrypoint extends the `ConnectorHubFunction` abstract class. +Note: the [func.yaml](func.yaml) entrypoint remains the class which extends `ConnectorHubFunction` +e.g. `cmd: com.fnproject.fn.examples.Function::handler` + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) + +```java +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.StreamingData; + +public class Function extends ConnectorHubFunction> { + + public StreamService streamService; + + public Function() { + this.streamService = new StreamService(); + } + + @Override + public void handler(ConnectorHubBatch> batch) { + for (StreamingData stream : batch.getBatch()) { + streamService.readStream(stream); + } + } +} +``` +The [ConnectorHubBatch.java](../../fn-events/src/main/java/com/fnproject/events/input/ConnectorHubBatch.java) +`batch` contains a list of events from Streaming as +specified in [Batch Settings](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#batch-settings). + +The class [StreamingData.java](../../fn-events/src/main/java/com/fnproject/events/input/sch/StreamingData.java) +represents the batch of Streaming Events. + +The [Employee.java](src/main/java/com/fnproject/fn/examples/Employee.java) represents the base64 encoded JSON +from value in each message. Note: Provide a String type if the message value is not JSON format. + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +`public class Function extends ConnectorHubFunction>`. + +To return an error response, throw RuntimeException.class. +Doing so will cause the Function to return a 502 [Retry policy](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#deactivate) + +## Test walkthrough + +Unit testing `ConnectorHubFunction` is supported with the `ConnectorHubTestFeature` and `FnTestingRule`. + +First of all, the class initializes the `FnTestingRule` harness, as explained +in [Testing Functions](../../docs/TestingFunctions.md). + +[FunctionTest.java](src/test/java/com/fnproject/fn/examples/FunctionTest.java) +```java +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.Date; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.StreamingData; +import com.fnproject.events.testing.ConnectorHubTestFeature; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithStreamingData() throws Exception { + + ConnectorHubBatch> event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static ConnectorHubBatch> createMinimalRequest() throws JsonProcessingException { + Employee employee = new Employee(); + employee.setName("foo"); + + StreamingData source = new StreamingData( + "stream-name", + "0", + null, + employee, + "3", + new Date(1764860467553L) + ); + ConnectorHubBatch> event = mock(ConnectorHubBatch.class); + + when(event.getBatch()).thenReturn(Collections.singletonList(source)); + return event; + } +} +``` + +Use `connectorHubTestFeature.givenEvent(event).enqueue();` to queue the request event +and invoke the Function with `fn.thenRun(Function.class, "handler");`. diff --git a/examples/connectorhub-streaming/func.yaml b/examples/connectorhub-streaming/func.yaml new file mode 100644 index 00000000..2b7cd061 --- /dev/null +++ b/examples/connectorhub-streaming/func.yaml @@ -0,0 +1,5 @@ +schema_version: 20180708 +name: connectorhub-streaming +version: 0.0.1 +runtime: java +cmd: com.fnproject.fn.examples.Function::handler diff --git a/examples/connectorhub-streaming/pom.xml b/examples/connectorhub-streaming/pom.xml new file mode 100644 index 00000000..afc256c8 --- /dev/null +++ b/examples/connectorhub-streaming/pom.xml @@ -0,0 +1,100 @@ + + + + + 4.0.0 + + + UTF-8 + UTF-8 + 1.0.0-SNAPSHOT + 2.16.1 + 4.0.0 + + + com.fnproject.fn.examples + connectorhub-streaming + 1.0.0-SNAPSHOT + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + fn-events + ${fdk.version} + + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + diff --git a/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/Employee.java b/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/Employee.java new file mode 100644 index 00000000..21079ea8 --- /dev/null +++ b/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/Employee.java @@ -0,0 +1,41 @@ +package com.fnproject.fn.examples; + +import java.util.Objects; + +public class Employee { + private String name; + + public Employee() {} + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Employee employee = (Employee) o; + return Objects.equals(name, employee.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + @Override + public String toString() { + return "Employee{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/Function.java b/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/Function.java new file mode 100644 index 00000000..0b7b8fbd --- /dev/null +++ b/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/Function.java @@ -0,0 +1,21 @@ +package com.fnproject.fn.examples; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.StreamingData; + +public class Function extends ConnectorHubFunction> { + + public StreamService streamService; + + public Function() { + this.streamService = new StreamService(); + } + + @Override + public void handler(ConnectorHubBatch> batch) { + for (StreamingData stream : batch.getBatch()) { + streamService.readStream(stream); + } + } +} \ No newline at end of file diff --git a/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/StreamService.java b/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/StreamService.java new file mode 100644 index 00000000..c6870ac0 --- /dev/null +++ b/examples/connectorhub-streaming/src/main/java/com/fnproject/fn/examples/StreamService.java @@ -0,0 +1,19 @@ +package com.fnproject.fn.examples; + +import com.fnproject.events.input.sch.StreamingData; + +public class StreamService { + + public void readStream(StreamingData streamingData) { + System.out.println(streamingData); + assert streamingData != null; + assert streamingData.getStream() != null; + assert streamingData.getPartition() != null; + assert streamingData.getValue() != null; + assert streamingData.getOffset() != null; + assert streamingData.getTimestamp() != null; + + Employee employee = streamingData.getValue(); + System.out.println(employee); + } +} diff --git a/examples/connectorhub-streaming/src/test/java/com/fnproject/fn/examples/FunctionTest.java b/examples/connectorhub-streaming/src/test/java/com/fnproject/fn/examples/FunctionTest.java new file mode 100644 index 00000000..bce8a569 --- /dev/null +++ b/examples/connectorhub-streaming/src/test/java/com/fnproject/fn/examples/FunctionTest.java @@ -0,0 +1,53 @@ +package com.fnproject.fn.examples; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.Collections; +import java.util.Date; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.StreamingData; +import com.fnproject.events.testing.ConnectorHubTestFeature; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final ConnectorHubTestFeature connectorHubTestFeature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithStreamingData() throws Exception { + + ConnectorHubBatch> event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static ConnectorHubBatch> createMinimalRequest() { + Employee employee = new Employee(); + employee.setName("foo"); + + StreamingData source = new StreamingData<>( + "stream-name", + "0", + null, + employee, + "3", + new Date(1764860467553L) + ); + ConnectorHubBatch> event = mock(ConnectorHubBatch.class); + + when(event.getBatch()).thenReturn(Collections.singletonList(source)); + return event; + } +} \ No newline at end of file diff --git a/examples/gradle-build/.dockerignore b/examples/gradle-build/.dockerignore new file mode 100644 index 00000000..531da109 --- /dev/null +++ b/examples/gradle-build/.dockerignore @@ -0,0 +1,4 @@ +.gradle +.idea +.git +build \ No newline at end of file diff --git a/examples/gradle-build/Dockerfile b/examples/gradle-build/Dockerfile new file mode 100644 index 00000000..cca84dbb --- /dev/null +++ b/examples/gradle-build/Dockerfile @@ -0,0 +1,36 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM gradle:4.5.1-jdk8 as build-stage +WORKDIR /function +# needed for gradle? +USER root +ENV GRADLE_USER_HOME /function/.gradle + +# Code build +# Copy any build files into the build container and build +COPY *.gradle /function/ +RUN ["gradle", "-s", "--no-daemon","--console","plain","cacheDeps"] + +# Copies build source into build container +COPY src /function/src + +RUN ["gradle", "-s", "--no-daemon","--console","plain","build"] +# Container build +FROM fnproject/fn-java-fdk:1.0.56 +WORKDIR /function +COPY --from=build-stage /function/build/libs/*.jar /function/build/deps/*.jar /function/app/ +CMD ["com.example.fn.HelloFunction::handleRequest"] diff --git a/examples/gradle-build/README.md b/examples/gradle-build/README.md new file mode 100644 index 00000000..3214e0b3 --- /dev/null +++ b/examples/gradle-build/README.md @@ -0,0 +1,14 @@ +# Fn Gradle + Java fdk example + +Fn uses Maven by default for builds. This is an example that uses Fn's `docker` runtime format to build a Java function using the [Fn Java FDK](https://github.com/fnproject/fdk-java). + +The example consists of a `Dockerfile` that builds the function using gradle and copies the function's dependencies to `build/deps` and a func.yaml that uses that `Dockerfile` to build the function. + +Note that fdk.versions are hard-coded in this example, you may need to update them manually to more recent version. + +Key points: + +* [Dockerfile](Dockerfile) - contains the containerised docker build (based on dockerhub library/gradle images) and image build - this includes the gradle invocation +* The `cacheDeps` task in `build.gradle` is invoked within the Dockerfile - The task pulls down dependencies into the container gradle cache to speed up docker builds. +* The `copyDeps` task in `build.gradle` copies the functions compile deps +* This uses JDK 8 by default - you can change this to Java 11 by changing : `FROM gradle:4.5.1-jdk8 as build-stage` to `FROM gradle:4.5.1-jre11 as build-stage` and `FROM fnproject/fn-java-fdk:1.0.85` to `FROM fnproject/fn-java-fdk:jre11-1.0.85` \ No newline at end of file diff --git a/examples/gradle-build/build.gradle b/examples/gradle-build/build.gradle new file mode 100644 index 00000000..6fb0925a --- /dev/null +++ b/examples/gradle-build/build.gradle @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'java' + +ext { + fdkVersion = '1.0.56' +} + +repositories { + mavenCentral() + maven { + url 'https://dl.bintray.com/fnproject/fnproject' + } +} + +dependencies { + runtime "com.fnproject.fn:api:$fdkVersion" + // runtime "com.fnproject.fn:runtime:$fdkVersion" // this is optional and included with its deps in the base image to reduce layer size + + testCompile "junit:junit:4.12" + testCompile "com.fnproject.fn:testing:$fdkVersion" +} + +task cacheDeps(type: Exec) { + configurations.testRuntime.files + commandLine 'echo', 'Downloaded all dependencies' +} + +task copyDeps(type: Copy) { + from configurations.compile + into "${project.buildDir}/deps" +} + +build.dependsOn copyDeps diff --git a/examples/gradle-build/func.yaml b/examples/gradle-build/func.yaml new file mode 100644 index 00000000..bc23a27a --- /dev/null +++ b/examples/gradle-build/func.yaml @@ -0,0 +1,20 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: gradle_build +version: 0.0.3 +runtime: docker +format: http diff --git a/examples/gradle-build/src/main/java/com/example/fn/HelloFunction.java b/examples/gradle-build/src/main/java/com/example/fn/HelloFunction.java new file mode 100644 index 00000000..c335e147 --- /dev/null +++ b/examples/gradle-build/src/main/java/com/example/fn/HelloFunction.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fn; + +public class HelloFunction { + + public String handleRequest(String input) { + String name = (input == null || input.isEmpty()) ? "world" : input; + return "Hello, " + name + "!"; + } + +} \ No newline at end of file diff --git a/examples/gradle-build/src/test/java/com/example/fn/HelloFunctionTest.java b/examples/gradle-build/src/test/java/com/example/fn/HelloFunctionTest.java new file mode 100644 index 00000000..5a036a2a --- /dev/null +++ b/examples/gradle-build/src/test/java/com/example/fn/HelloFunctionTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fn; + +import static org.junit.Assert.*; + +public class HelloFunctionTest { + + @Rule + public final FnTestingRule testing = FnTestingRule.createDefault(); + + @Test + public void shouldReturnGreeting() { + testing.givenEvent().enqueue(); + testing.thenRun(HelloFunction.class, "handleRequest"); + + FnResult result = testing.getOnlyResult(); + assertEquals("Hello, world!", result.getBodyAsString()); + } + +} \ No newline at end of file diff --git a/examples/notifications/README.md b/examples/notifications/README.md new file mode 100644 index 00000000..2966b991 --- /dev/null +++ b/examples/notifications/README.md @@ -0,0 +1,116 @@ +# Example Fn Java FDK : Notifications + +This example provides a Function to consume a Notification. +The function accepts a typed message event. + +## Source + +* Notifications appends metadata to the headers [Standard header metadata](https://docs.oracle.com/en-us/iaas/Content/Notification/Concepts/notificationoverview.htm#hownw) +* The Notification message body is a user specified Schema or String. +Note: This library currently supports JSON and String types. +* Limits for Notifications [Notification limits](https://docs.oracle.com/en-us/iaas/Content/Notification/Concepts/notificationoverview.htm#limits) + +## Dependencies +* [fn-events](../../fn-events) for NotificationFunction classes. +* [fn-events-testing](../../fn-events-testing) for NotificationFunction testing library. + +## Demonstrated FDK features + +This example showcases how to use the fn-event NotificationFunction to +use a Function to consume messages from a Notification topic. + +## Step by step + +Set up the Notification Topic with Function Subscription: +* [Setup default policies](https://docs.oracle.com/en-us/iaas/Content/Security/Reference/notifications_security.htm#iam-policies__subs) +* [create subscription function](https://docs.oracle.com/en-us/iaas/Content/Notification/Tasks/create-subscription-function.htm) + +The Function entrypoint extends the `NotificationFunction` abstract class. +Note: the [func.yaml](func.yaml) entrypoint remains the class which extends `NotificationFunction` +e.g. `cmd: com.fnproject.fn.examples.Function::handler` + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) + +```java +package com.fnproject.fn.examples; + +import com.fnproject.events.NotificationFunction; +import com.fnproject.events.input.NotificationMessage; + +public class Function extends NotificationFunction { + + public NotificationService notificationService; + + public Function() { + this.notificationService = new NotificationService(); + } + + @Override + public void handler(NotificationMessage content) { + notificationService.readNotification(content); + } +} +``` +The [NotificationMessage.java](../../fn-events/src/main/java/com/fnproject/events/input/NotificationMessage.java) +`content` contains the message body. + +The class [Employee.java](src/main/java/com/fnproject/fn/examples/Employee.java) is +the schema for the message content. + +[Function.java](src/main/java/com/fnproject/fn/examples/Function.java) +`public class Function extends NotificationFunction`. + +To return an error response, throw RuntimeException.class. +Doing so will cause the Function to return a 502 [Retry policy](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm#deactivate) + +## Test walkthrough + +Unit testing `NotificationFunction` is supported with the `NotificationTestFeature` and `FnTestingRule`. + +First of all, the class initializes the `FnTestingRule` harness, as explained +in [Testing Functions](../../docs/TestingFunctions.md). + +[FunctionTest.java](src/test/java/com/fnproject/fn/examples/FunctionTest.java) +```java +package com.fnproject.fn.examples; + +import static org.junit.Assert.assertEquals; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.events.testing.NotificationTestFeature; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final NotificationTestFeature connectorHubTestFeature = NotificationTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithLoggingData() throws Exception { + NotificationMessage event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static NotificationMessage createMinimalRequest() { + Employee employee = new Employee(); + employee.setName("foo"); + + return new NotificationMessage<>(employee, Headers.emptyHeaders()); + } +} +``` + +Use `connectorHubTestFeature.givenEvent(event).enqueue();` to queue the request event +and invoke the Function with `fn.thenRun(Function.class, "handler");`. + +To verify the Subscription is working, check [Notification metrics](https://docs.oracle.com/en-us/iaas/Content/Notification/Tasks/view-chart-resource.htm#top) \ No newline at end of file diff --git a/examples/notifications/func.yaml b/examples/notifications/func.yaml new file mode 100644 index 00000000..e065909d --- /dev/null +++ b/examples/notifications/func.yaml @@ -0,0 +1,5 @@ +schema_version: 20180708 +name: notifications +version: 0.0.1 +runtime: java +cmd: com.fnproject.fn.examples.Function::handler diff --git a/examples/notifications/pom.xml b/examples/notifications/pom.xml new file mode 100644 index 00000000..e470aa39 --- /dev/null +++ b/examples/notifications/pom.xml @@ -0,0 +1,112 @@ + + + + + 4.0.0 + + + UTF-8 + UTF-8 + 1.0.0-SNAPSHOT + 2.16.1 + 4.0.0 + + + com.fnproject.fn.examples + notifications + 1.0.0-SNAPSHOT + + + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + fn-events + ${fdk.version} + + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + + + junit + junit + 4.13.2 + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + + + + + faas-release-maven-local + https://artifactory-builds.oci.oraclecorp.com/faas-release-maven-local + + true + + + true + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.0 + + 1.8 + 1.8 + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + true + + + + + diff --git a/examples/notifications/src/main/java/com/fnproject/fn/examples/Employee.java b/examples/notifications/src/main/java/com/fnproject/fn/examples/Employee.java new file mode 100644 index 00000000..21079ea8 --- /dev/null +++ b/examples/notifications/src/main/java/com/fnproject/fn/examples/Employee.java @@ -0,0 +1,41 @@ +package com.fnproject.fn.examples; + +import java.util.Objects; + +public class Employee { + private String name; + + public Employee() {} + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Employee employee = (Employee) o; + return Objects.equals(name, employee.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + @Override + public String toString() { + return "Employee{" + + "name='" + name + '\'' + + '}'; + } +} diff --git a/examples/notifications/src/main/java/com/fnproject/fn/examples/Function.java b/examples/notifications/src/main/java/com/fnproject/fn/examples/Function.java new file mode 100644 index 00000000..923c650c --- /dev/null +++ b/examples/notifications/src/main/java/com/fnproject/fn/examples/Function.java @@ -0,0 +1,18 @@ +package com.fnproject.fn.examples; + +import com.fnproject.events.NotificationFunction; +import com.fnproject.events.input.NotificationMessage; + +public class Function extends NotificationFunction { + + public NotificationService notificationService; + + public Function() { + this.notificationService = new NotificationService(); + } + + @Override + public void handler(NotificationMessage content) { + notificationService.readNotification(content); + } +} \ No newline at end of file diff --git a/examples/notifications/src/main/java/com/fnproject/fn/examples/NotificationService.java b/examples/notifications/src/main/java/com/fnproject/fn/examples/NotificationService.java new file mode 100644 index 00000000..3e1ed461 --- /dev/null +++ b/examples/notifications/src/main/java/com/fnproject/fn/examples/NotificationService.java @@ -0,0 +1,10 @@ +package com.fnproject.fn.examples; + +import com.fnproject.events.input.NotificationMessage; + +public class NotificationService { + + public void readNotification(NotificationMessage notification) { + System.out.println(notification); + } +} diff --git a/examples/notifications/src/test/java/com/fnproject/fn/examples/FunctionTest.java b/examples/notifications/src/test/java/com/fnproject/fn/examples/FunctionTest.java new file mode 100644 index 00000000..3afca9e1 --- /dev/null +++ b/examples/notifications/src/test/java/com/fnproject/fn/examples/FunctionTest.java @@ -0,0 +1,36 @@ +package com.fnproject.fn.examples; + +import static org.junit.Assert.assertEquals; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.events.testing.NotificationTestFeature; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class FunctionTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + private final NotificationTestFeature connectorHubTestFeature = NotificationTestFeature.createDefault(fn); + + @Test + public void testInvokeFunctionWithLoggingData() throws Exception { + NotificationMessage event = createMinimalRequest(); + connectorHubTestFeature.givenEvent(event).enqueue(); + + fn.thenRun(Function.class, "handler"); + + FnResult result = fn.getOnlyResult(); + assertEquals(200, result.getStatus().getCode()); + } + + private static NotificationMessage createMinimalRequest() { + Employee employee = new Employee(); + employee.setName("foo"); + + return new NotificationMessage<>(employee, Headers.emptyHeaders()); + } +} \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index 219291f4..34a63435 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -1,10 +1,29 @@ + + 4.0.0 pom fdk-examples + fdk-examples com.fnproject.fn.examples @@ -13,10 +32,6 @@ 1.0.0-SNAPSHOT - - UTF-8 - - string-reverse regex-query @@ -24,6 +39,12 @@ qr-code --> async-thumbnails + apigateway-event + connectorhub-monitoring + connectorhub-logging + connectorhub-streaming + connectorhub-queue + notifications @@ -31,7 +52,6 @@ org.apache.maven.plugins maven-deploy-plugin - 2.8.2 true diff --git a/examples/qr-code/README.md b/examples/qr-code/README.md index d511c970..eda10787 100644 --- a/examples/qr-code/README.md +++ b/examples/qr-code/README.md @@ -31,8 +31,8 @@ $ fn build Create an app and route to host the function ```bash -$ fn apps create qr-app -$ fn routes create qr-app /qr +$ fn create app qr-app +$ fn create route qr-app /qr ``` Invoke the function to create a QR code @@ -54,21 +54,22 @@ of the example function. The body of the function is shown below: ```java -public OutputEvent create(InputEvent event) throws MalformedURLException, UnsupportedEncodingException { - String decodedUrl = URLDecoder.decode(event.getRequestUrl(), "utf-8"); - QueryParameters params = getParams(decodedUrl); - ImageType type = getFormat(params.getFirst("format").orElse("png")); - String contents = params.getFirst("contents").orElse(""); - - ByteArrayOutputStream stream = QRCode.from(contents).to(type).stream(); - System.err.println("Generated QR Code for contents: " + contents); - return OutputEvent.fromBytes(stream.toByteArray(), true, getMimeType(type)); -} + + public byte[] create(HTTPGatewayContext hctx) { + ImageType type = getFormat(hctx.getQueryParameters().get("format").orElse(defaultFormat)); + System.err.println("Default format: " + type.toString()); + String contents = hctx.getQueryParameters().get("contents").orElseThrow(() -> new RuntimeException("Contents must be provided to the QR code")); + + ByteArrayOutputStream stream = QRCode.from(contents).to(type).stream(); + System.err.println("Generated QR Code for contents: " + contents); + + hctx.setResponseHeader("Content-Type", getMimeType(type)); + return stream.toByteArray(); + } + ``` -The fn Java FDK facilitates access to the internal events representing the -invocation of the function, `InputEvent`, and response of the function, -`OutputEvent`, for more fine grained control of the platform. See +The fn Java FDK facilitates access to the HTTP context of events triggered from HTTP gateways via `HTTPGatewayContext` , and response of the function as a bye array, for more fine grained control of the platform. See [Data Binding](/docs/DataBinding.md) for further information on the types of input that the fn Java FDK provides. @@ -116,17 +117,17 @@ this to handle invocations of functions and retrieving function results ```java ... - @Test - public void textHelloWorld() throws Exception { - fn.givenEvent() - .withRequestUrl("http://www.example.com/qr?contents=" + URLEncoder.encode("hello world", "utf-8")) - .withMethod("GET") - .enqueue(); - fn.thenRun(QRGen.class, "create"); - - assertArrayEquals(readTestFile("qr-code-text-hello-world.png"), fn.getOnlyResult().getBodyAsBytes()); - } -... + @Test + public void textHelloWorld() throws Exception { + String content = "hello world"; + fn.givenEvent() + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=hello+world&format=png") + .withHeader("Fn-Http-Method","GET") + .enqueue(); + fn.thenRun(QRGen.class, "create"); + + assertEquals(content, decode(fn.getOnlyResult().getBodyAsBytes())); + } ``` Input events are constructed using `fn.givenEvent()` providing an `FnEventBuilder` diff --git a/examples/qr-code/example.html b/examples/qr-code/example.html index fb7d1f6a..d4d964c2 100644 --- a/examples/qr-code/example.html +++ b/examples/qr-code/example.html @@ -1,3 +1,21 @@ + + diff --git a/examples/qr-code/func.yaml b/examples/qr-code/func.yaml index ccff7c49..4ca1732e 100644 --- a/examples/qr-code/func.yaml +++ b/examples/qr-code/func.yaml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + name: fn-example/qr-code-gen version: 0.0.1 runtime: java diff --git a/examples/qr-code/pom.xml b/examples/qr-code/pom.xml index e7941657..b9cec937 100644 --- a/examples/qr-code/pom.xml +++ b/examples/qr-code/pom.xml @@ -1,4 +1,22 @@ + + @@ -7,7 +25,7 @@ UTF-8 - 1.0.0-SNAPSHOT + 1.0.0-SNAPSHOT com.fnproject.fn.examples @@ -24,19 +42,25 @@ com.fnproject.fn api - ${fnproject.version} + ${fdk.version} junit junit - 4.12 + 4.13.1 test com.fnproject.fn testing - ${fnproject.version} + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} test @@ -45,7 +69,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.3 + 3.8.0 1.8 1.8 diff --git a/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java b/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java index a95caa3d..99b886b9 100644 --- a/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java +++ b/examples/qr-code/src/main/java/com/fnproject/fn/examples/QRGen.java @@ -1,13 +1,27 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; import net.glxn.qrgen.core.image.ImageType; import net.glxn.qrgen.javase.QRCode; -import java.io.*; -import java.net.MalformedURLException; +import java.io.ByteArrayOutputStream; public class QRGen { private final String defaultFormat; @@ -16,19 +30,20 @@ public QRGen(RuntimeContext ctx) { defaultFormat = ctx.getConfigurationByKey("FORMAT").orElse("png"); } - public OutputEvent create(InputEvent event) throws MalformedURLException, UnsupportedEncodingException { - ImageType type = getFormat(event.getQueryParameters().get("format").orElse(defaultFormat)); + public byte[] create(HTTPGatewayContext hctx) { + ImageType type = getFormat(hctx.getQueryParameters().get("format").orElse(defaultFormat)); System.err.println("Default format: " + type.toString()); - String contents = event.getQueryParameters().get("contents").orElseThrow(() -> new RuntimeException("Contents must be provided to the QR code")); + String contents = hctx.getQueryParameters().get("contents").orElseThrow(() -> new RuntimeException("Contents must be provided to the QR code")); ByteArrayOutputStream stream = QRCode.from(contents).to(type).stream(); System.err.println("Generated QR Code for contents: " + contents); - return OutputEvent.fromBytes(stream.toByteArray(), OutputEvent.SUCCESS, getMimeType(type)); + hctx.setResponseHeader("Content-Type", getMimeType(type)); + return stream.toByteArray(); } private ImageType getFormat(String extension) { - switch(extension.toLowerCase()) { + switch (extension.toLowerCase()) { case "png": return ImageType.PNG; case "jpg": @@ -43,8 +58,8 @@ private ImageType getFormat(String extension) { } } - private String getMimeType(ImageType type) { - switch(type) { + private String getMimeType(ImageType type) { + switch (type) { case JPG: return "image/jpeg"; case GIF: @@ -56,5 +71,5 @@ private String getMimeType(ImageType type) { default: throw new RuntimeException("Invalid ImageType: " + type); } - } + } } diff --git a/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java b/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java index d727e941..cb376482 100644 --- a/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java +++ b/examples/qr-code/src/test/java/com/fnproject/fn/examples/QRGenTest.java @@ -1,7 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; import com.fnproject.fn.testing.FnTestingRule; -import com.google.zxing.*; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; import com.google.zxing.client.j2se.BufferedImageLuminanceSource; import com.google.zxing.common.HybridBinarizer; import com.google.zxing.qrcode.QRCodeReader; @@ -23,11 +42,9 @@ public class QRGenTest { public void textHelloWorld() throws Exception { String content = "hello world"; fn.givenEvent() - .withRequestUrl("http://www.example.com/qr") - .withQueryParameter("contents", content) - .withQueryParameter("format", "png") - .withMethod("GET") - .enqueue(); + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=hello+world&format=png") + .withHeader("Fn-Http-Method","GET") + .enqueue(); fn.thenRun(QRGen.class, "create"); assertEquals(content, decode(fn.getOnlyResult().getBodyAsBytes())); @@ -37,10 +54,9 @@ public void textHelloWorld() throws Exception { public void phoneNumber() throws Exception { String telephoneNumber = "tel:0-12345-67890"; fn.givenEvent() - .withRequestUrl("http://www.example.com/qr") - .withQueryParameter("contents", telephoneNumber) - .withMethod("GET") - .enqueue(); + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=tel:0-12345-67890") + .withHeader("Fn-Http-Method","GET") + .enqueue(); fn.thenRun(QRGen.class, "create"); assertEquals(telephoneNumber, decode(fn.getOnlyResult().getBodyAsBytes())); @@ -51,14 +67,14 @@ public void formatConfigurationIsUsedIfNoFormatIsProvided() throws Exception { String contents = "hello world"; fn.setConfig("FORMAT", "jpg"); fn.givenEvent() - .withRequestUrl("http://www.example.com/qr") - .withQueryParameter("contents", contents) - .withMethod("GET") - .enqueue(); + .withHeader("Fn-Http-Request-Url", "http://www.example.com/qr?contents=hello+world") + .withHeader("Fn-Http-Method","GET") + .enqueue(); fn.thenRun(QRGen.class, "create"); assertEquals(contents, decode(fn.getOnlyResult().getBodyAsBytes())); } + private String decode(byte[] imageBytes) throws IOException, NotFoundException, ChecksumException, FormatException { BinaryBitmap bitmap = readToBitmap(imageBytes); return new QRCodeReader().decode(bitmap).getText(); diff --git a/examples/regex-query/README.md b/examples/regex-query/README.md index a9d67155..f7a21c6e 100644 --- a/examples/regex-query/README.md +++ b/examples/regex-query/README.md @@ -33,8 +33,8 @@ $ fn build Create an app and route to host the function ```bash -$ fn apps create regex-query -$ fn routes create regex-query /query +$ fn create app regex-query +$ fn create route regex-query /query ``` Invoke the function to perform a regex search diff --git a/examples/regex-query/func.yaml b/examples/regex-query/func.yaml index b216f06e..d14a3f37 100644 --- a/examples/regex-query/func.yaml +++ b/examples/regex-query/func.yaml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + name: fn-example/regex-query version: 0.0.1 runtime: java diff --git a/examples/regex-query/pom.xml b/examples/regex-query/pom.xml index 23961362..ac7bead1 100644 --- a/examples/regex-query/pom.xml +++ b/examples/regex-query/pom.xml @@ -1,4 +1,22 @@ + + @@ -6,8 +24,10 @@ UTF-8 - 1.0.0-SNAPSHOT - 2.8.9 + UTF-8 + + 1.0.0-SNAPSHOT + 2.16.1 com.fnproject.fn.examples @@ -23,18 +43,24 @@ com.fasterxml.jackson.core jackson-annotations - ${jackson.version} + 2.13.4 junit junit - 4.12 + 4.13.2 + test + + + com.fnproject.fn + testing-core + ${fdk.version} test com.fnproject.fn - testing - ${fnproject.version} + testing-junit4 + ${fdk.version} test @@ -50,7 +76,7 @@ org.apache.maven.plugins maven-compiler-plugin - 3.3 + 3.8.0 1.8 1.8 @@ -66,11 +92,4 @@ - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - diff --git a/examples/regex-query/src/main/java/com/fnproject/fn/examples/Match.java b/examples/regex-query/src/main/java/com/fnproject/fn/examples/Match.java index d4b1188c..f2bb4f90 100644 --- a/examples/regex-query/src/main/java/com/fnproject/fn/examples/Match.java +++ b/examples/regex-query/src/main/java/com/fnproject/fn/examples/Match.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/examples/regex-query/src/main/java/com/fnproject/fn/examples/Query.java b/examples/regex-query/src/main/java/com/fnproject/fn/examples/Query.java index e927400e..fe5a9256 100644 --- a/examples/regex-query/src/main/java/com/fnproject/fn/examples/Query.java +++ b/examples/regex-query/src/main/java/com/fnproject/fn/examples/Query.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/examples/regex-query/src/main/java/com/fnproject/fn/examples/RegexQuery.java b/examples/regex-query/src/main/java/com/fnproject/fn/examples/RegexQuery.java index cbfd993e..aab1c7bb 100644 --- a/examples/regex-query/src/main/java/com/fnproject/fn/examples/RegexQuery.java +++ b/examples/regex-query/src/main/java/com/fnproject/fn/examples/RegexQuery.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; import java.util.ArrayList; diff --git a/examples/regex-query/src/main/java/com/fnproject/fn/examples/Response.java b/examples/regex-query/src/main/java/com/fnproject/fn/examples/Response.java index af939b9f..0f7330a5 100644 --- a/examples/regex-query/src/main/java/com/fnproject/fn/examples/Response.java +++ b/examples/regex-query/src/main/java/com/fnproject/fn/examples/Response.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java b/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java index e5a44626..d0e241d5 100644 --- a/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java +++ b/examples/regex-query/src/test/java/com/fnproject/fn/examples/RegexQueryTests.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.examples; import com.fnproject.fn.testing.FnTestingRule; @@ -15,7 +31,6 @@ public void matchingSingleCharacter() throws JSONException { String text = "a"; String regex = "."; fn.givenEvent() - .withMethod("POST") .withBody(String.format("{\"text\": \"%s\", \"regex\": \"%s\"}", text, regex)) .enqueue(); @@ -34,7 +49,6 @@ public void matchingSingleCharacterMultipleTimes() throws JSONException { String text = "abc"; String regex = "."; fn.givenEvent() - .withMethod("POST") .withBody(String.format("{\"text\": \"%s\", \"regex\": \"%s\"}", text, regex)) .enqueue(); diff --git a/examples/string-reverse/README.md b/examples/string-reverse/README.md index 9c52dae4..97eb79a0 100644 --- a/examples/string-reverse/README.md +++ b/examples/string-reverse/README.md @@ -1,71 +1,75 @@ -# Example oFunctions Project: String Reverse +# Example Java Function: String Reverse -This example provides an HTTP endpoint for reversing strings +This example provides an HTTP trigger endpoint for reversing strings. ```bash -$ curl -d "Hello, World!" "http://localhost:8080/r/string-reverse-app/reverse" -!dlroW ,olleH +$ curl -d "Hello World" http://localhost:8080/t/string-reverse-app/string-reverse +dlroW olleH ``` ## Demonstrated FDK features -This example uses **no** features of the fn Java FDK; in fact it doesn't have -a dependency on the fn Java FDK, it just plain old Java code. +This example uses **none** of the Fn Java FDK features, in fact it doesn't have +any dependency on the Fn Java FDK. It is just plain old Java code. ## Step by step -Ensure you have the functions server running using, this will host your -function and provide the HTTP endpoints that invoke it: +Ensure you have the Fn server running to host your +function and provide the HTTP endpoint that invokes it: -```bash +(1) Start the server + +```sh $ fn start ``` -Build the function locally +(2) Create an app for the function -```bash -$ fn build +```sh +$ fn create app string-reverse-app ``` -Create an app and route to host the function +(3) Deploy the function to your app from the `string-reverse` directory. -```bash -$ fn apps create string-reverse-app -$ fn routes create string-reverse-app /reverse +```sh +fn deploy --app string-reverse-app --local +``` + +(4) Invoke the function and reverse the string. + +```sh +echo "Hello World" | fn invoke string-reverse-app string-reverse +dlroW olleH ``` -Invoke the function to reverse a string +(5) Invoke the function using curl and a trigger to reverse a string. ```bash -$ curl -d "Hello, World!" "http://localhost:8080/r/string-reverse-app/reverse" -!dlroW ,olleH +$ curl -d "Hello World" http://localhost:8080/t/string-reverse-app/string-reverse +dlroW olleH ``` ## Code walkthrough The entrypoint to the function is specified in `func.yaml` in the `cmd` key. -It is set this to `com.fnproject.fn.examples.StringReverse::reverse`. The whole class +It is set this to `com.example.fn.StringReverse::reverse`. The whole class `StringReverse` is shown below: ```java -package com.fnproject.fn.examples; +package com.example.fn; public class StringReverse { public String reverse(String str) { - StringBuilder builder = new StringBuilder(); - for (int i = str.length() - 1; i >= 0; i--) { - builder.append(str.charAt(i)); - } - return builder.toString(); + return new StringBuilder(str).reverse().toString(); } } ``` -As you can see, this is plain java with no references to the fn API. The -fn Java FDK handles the marshalling of the HTTP body into the `str` +As you can see, this is plain java with no references to the Fn API. The +Fn Java FDK handles the marshalling of the HTTP body into the `str` parameter as well as the marshalling of the returned reversed string into the HTTP response body (see [Data Binding](/docs/DataBinding.md) for more information on how marshalling is performed). diff --git a/examples/string-reverse/func.yaml b/examples/string-reverse/func.yaml index 197eebe2..06045be1 100644 --- a/examples/string-reverse/func.yaml +++ b/examples/string-reverse/func.yaml @@ -1,7 +1,27 @@ -name: fn-example/string-reverse -version: 0.0.1 +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +schema_version: 20180708 +name: string-reverse +version: 0.0.2 runtime: java -timeout: 30 -format: http -cmd: com.fnproject.fn.examples.StringReverse::reverse -path: /reverse +build_image: fnproject/fn-java-fdk-build:jdk11-1.0.87 +run_image: fnproject/fn-java-fdk:jre11-1.0.87 +cmd: com.example.fn.StringReverse::reverse +triggers: +- name: string-reverse + type: http + source: /string-reverse diff --git a/examples/string-reverse/pom.xml b/examples/string-reverse/pom.xml index 4c380ea1..472b033b 100644 --- a/examples/string-reverse/pom.xml +++ b/examples/string-reverse/pom.xml @@ -1,23 +1,57 @@ + + 4.0.0 - - UTF-8 + 1.0.0-SNAPSHOT - com.fnproject.fn.examples string-reverse 1.0.0-SNAPSHOT + + com.fnproject.fn + api + ${fdk.version} + + + com.fnproject.fn + testing-core + ${fdk.version} + test + + + com.fnproject.fn + testing-junit4 + ${fdk.version} + test + junit junit - 4.12 + 4.13.2 + test @@ -28,8 +62,8 @@ maven-compiler-plugin 3.3 - 1.8 - 1.8 + 8 + 8 @@ -40,13 +74,14 @@ true + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + false + + - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - diff --git a/examples/string-reverse/src/main/java/com/example/fn/StringReverse.java b/examples/string-reverse/src/main/java/com/example/fn/StringReverse.java new file mode 100644 index 00000000..6c66f8b2 --- /dev/null +++ b/examples/string-reverse/src/main/java/com/example/fn/StringReverse.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fn; + +public class StringReverse { + public String reverse(String str) { + return new StringBuilder(str).reverse().toString(); + } +} diff --git a/examples/string-reverse/src/main/java/com/fnproject/fn/examples/StringReverse.java b/examples/string-reverse/src/main/java/com/fnproject/fn/examples/StringReverse.java deleted file mode 100644 index e95ce08e..00000000 --- a/examples/string-reverse/src/main/java/com/fnproject/fn/examples/StringReverse.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.fnproject.fn.examples; - -public class StringReverse { - public String reverse(String str) { - StringBuilder builder = new StringBuilder(); - for (int i = str.length() - 1; i >= 0; i--) { - builder.append(str.charAt(i)); - } - return builder.toString(); - } -} diff --git a/examples/string-reverse/src/test/java/com/example/fn/testing/StringReverseTest.java b/examples/string-reverse/src/test/java/com/example/fn/testing/StringReverseTest.java new file mode 100644 index 00000000..19c2af40 --- /dev/null +++ b/examples/string-reverse/src/test/java/com/example/fn/testing/StringReverseTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.fn.testing; + +import com.example.fn.StringReverse; +import org.junit.Test; + +import static junit.framework.TestCase.assertEquals; + +public class StringReverseTest { + private StringReverse stringReverse = new StringReverse(); + + @Test + public void reverseEmptyString() { + assertEquals("", reverse("")); + } + + @Test + public void reverseOfSingleCharacter() { + assertEquals("a", reverse("a")); + } + + @Test + public void reverseHelloIsOlleh() { + assertEquals("olleh", reverse("hello")); + } + + private String reverse(String str) { + return stringReverse.reverse(str); + } +} diff --git a/examples/string-reverse/src/test/java/com/fnproject/examples/StringReverseTest.java b/examples/string-reverse/src/test/java/com/fnproject/examples/StringReverseTest.java deleted file mode 100644 index 9f67dd2c..00000000 --- a/examples/string-reverse/src/test/java/com/fnproject/examples/StringReverseTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.fnproject.examples; - -import com.fnproject.fn.examples.StringReverse; -import org.junit.Test; - -import static junit.framework.TestCase.assertEquals; - -public class StringReverseTest { - private StringReverse stringReverse = new StringReverse(); - - @Test - public void reverseEmptyString() { - assertEquals("", reverse("")); - } - - @Test - public void reverseOfSingleCharacter() { - assertEquals("a", reverse("a")); - } - - @Test - public void reverseHelloIsOlleh() { - assertEquals("olleh", reverse("hello")); - } - - private String reverse(String str) { - return stringReverse.reverse(str); - } -} diff --git a/experimental-native-image-support/README.md b/experimental-native-image-support/README.md new file mode 100644 index 00000000..e7200e08 --- /dev/null +++ b/experimental-native-image-support/README.md @@ -0,0 +1,50 @@ +# Experimental support tools for functions native images + +This is an optional module that can be added to native-image builds that resolves some common issues related to +native-image handling in oracle functions. + +This library is _experimental_ - it may change in behaviour and may not work in many cases. + +Currently, this contains graal native-image configuration files which enable common use cases for : + +* Oracle Cloud Infrastructure java client support in native images +* Graal native reflection config for general Jersey client support (specifically tested for the OCI client use case, may work in other cases) +* Graal native reflection config for BouncyCastle crypto in native images + +It also includes : + +* A dynamic graal-native feature which automatically adds classes referenced in Jackson-databind annotations - this removes the need to have to manually add model classes that are referenced via Jackson annotations like `@JsonSubTypes.Type` `@JsonDeserlize` etc. + +# Enabling the feature in a native build: + +Generate a native build function (replace 1.0.121 with the appropriate fdk-java version): + +``` +fn init --init-image fnproject/fn-java-native-init:jdk11-1.0.121 graalfn +``` + +Edit your pom file and add this library as a dependency: + +```xml + + com.fnproject.fn + experimental-native-image-support + ${fdk.version} + runtime + +``` + +Edit the generated `Dockerfile` and add the following to enable the feature: + +``` + --features=com.fnproject.fn.nativeimagesupport.JacksonFeature \ +``` +You may also need to add the following flags if they are not already set: +``` + --allow-incomplete-classpath \ + --enable-all-security-services \ + --enable-url-protocols=https \ + --report-unsupported-elements-at-runtime \ +``` + +You may see "WARNING:..." messages during the build, these are expected and should not cause issues. diff --git a/experimental-native-image-support/pom.xml b/experimental-native-image-support/pom.xml new file mode 100644 index 00000000..31fe7592 --- /dev/null +++ b/experimental-native-image-support/pom.xml @@ -0,0 +1,80 @@ + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + experimental-native-image-support + experimental-native-image-support + + + + com.fasterxml.jackson.core + jackson-databind + + + + junit + junit + test + + + org.graalvm.nativeimage + graal-hotspot-library + ${graalvm.version} + provided + true + + + org.assertj + assertj-core + test + + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + diff --git a/experimental-native-image-support/src/main/java/com/fnproject/fn/nativeimagesupport/JacksonFeature.java b/experimental-native-image-support/src/main/java/com/fnproject/fn/nativeimagesupport/JacksonFeature.java new file mode 100644 index 00000000..a5d7e6d1 --- /dev/null +++ b/experimental-native-image-support/src/main/java/com/fnproject/fn/nativeimagesupport/JacksonFeature.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2019, 2020, 2021 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.nativeimagesupport; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.annotation.JacksonAnnotation; +import com.oracle.svm.reflect.hosted.ReflectionFeature; +import org.graalvm.nativeimage.ImageSingletons; +import org.graalvm.nativeimage.hosted.Feature; +import org.graalvm.nativeimage.impl.RuntimeReflectionSupport; + +/** + * This is a graal native-image feature that automatically includes any classes referenced as literals in Jackson Annotations + * from included classes. + *

+ * The process assumes that the following classes require full reflective acceess (all fields/methods/constructors) : + *

    + *
  • Classes with Jackson annotations (any jackson annotation on the class or in a member)
  • + *
  • Any Class literals referenced from jackson annotations or their descendents (e.g. Serializers, dispatch types)
  • + *
+ *

+ * This is not likely to be complete and may skip annotations in some cases, notably there are cases where super classes may not be correctly accounted for. + */ +public class JacksonFeature implements Feature { + + public JacksonFeature() { + System.out.println("JacksonFeature: FnProject experimental Jackson feature loaded"); + System.out.println("JacksonFeature: Graal native image support is *experimental* it may not be stable and there may be cases where it does not work as expected"); + } + + private static final String JACKSON_PACKAGE_PREFIX = "com.fasterxml.jackson"; + + + private static boolean shouldIncludeClass(Class clz) { + return !clz.isInterface() && + clz != Void.class + && !clz.getPackage().getName().startsWith(JACKSON_PACKAGE_PREFIX); + } + + private static boolean isJacksonAnnotation(Annotation a) { + return a.annotationType().getAnnotation(JacksonAnnotation.class) != null; + } + + protected static Stream> extractLiteralAnnotationRefs(Annotation a) { + Class aClass = a.annotationType(); + return Arrays.stream(aClass.getDeclaredMethods()) + .flatMap(m -> { + Object val; + // get annotation attribute value + try { + val = m.invoke(a); + } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) { + throw new IllegalStateException("Failed to retrieve annotation value from annotation" + a + " method " + m, e); + } + + // technically annotations can't be null but just in case + if (val == null) { + return Stream.empty(); + } + + if (val.getClass().isAnnotation()) { // annotation param on an annotation - descend + return extractLiteralAnnotationRefs((Annotation) val); + } else if (val.getClass().isArray()) { + Class innerType = val.getClass().getComponentType(); + if (innerType.isAnnotation()) { // list of annotations - descend + return Arrays.stream((Annotation[]) val) + .flatMap(JacksonFeature::extractLiteralAnnotationRefs); + } + return Arrays.stream((Object[]) val) // add class literals in array ref + .filter(arrayVal -> arrayVal instanceof Class).map(arrayVal -> (Class) arrayVal); + } else if (val instanceof Class) { + return Stream.of((Class) val); + } + return Stream.empty(); + }); + } + + + // VisibleForTesting + protected static Stream> expandClassesToMarkForReflection(Class clazz) { + List jacksonAnnotations; + try { + jacksonAnnotations = Stream.concat(Stream.concat( + Arrays.stream(clazz.getAnnotations()), + Arrays.stream(clazz.getDeclaredFields()).flatMap(f -> Arrays.stream(f.getAnnotations()))), + Arrays.stream(clazz.getDeclaredMethods()).flatMap(m -> Arrays.stream(m.getAnnotations())) + ).filter(JacksonFeature::isJacksonAnnotation).collect(Collectors.toList()); + } catch (NoClassDefFoundError ignored) { + // we skip the whole class if any of its members are unresolvable - this is assumed safe as jackson won't be able to load the class here anyway + return Stream.empty(); + } + + // if no jackson classes present, skip the whole class + if (jacksonAnnotations.isEmpty()) { + return Stream.empty(); + } + + // otherwise include the class and any descendent classes referenced within those annotations + return Stream.concat(Stream.of(clazz), jacksonAnnotations.stream() + .flatMap(JacksonFeature::extractLiteralAnnotationRefs)) + .filter(JacksonFeature::shouldIncludeClass); + + } + + @Override + public List> getRequiredFeatures() { + List> fs = new ArrayList<>(); + fs.add(ReflectionFeature.class); + return fs; + } + + @Override + public void beforeAnalysis(BeforeAnalysisAccess access) { + + ClassLoader cl = access.getApplicationClassLoader(); + RuntimeReflectionSupport rrs = ImageSingletons.lookup(RuntimeReflectionSupport.class); + + access.registerSubtypeReachabilityHandler((acc, sourceClazz) -> { + expandClassesToMarkForReflection(sourceClazz) + .forEach((referencedClazz) -> { + System.out.println("JacksonFeature: adding extra Jackson annotated " + referencedClazz); + acc.registerAsUsed(referencedClazz); + acc.registerAsInHeap(referencedClazz); + rrs.register(referencedClazz); + rrs.register(referencedClazz.getDeclaredConstructors()); + rrs.register(referencedClazz.getDeclaredMethods()); + Arrays.stream(referencedClazz.getDeclaredFields()).forEach(f -> rrs.register(false, f)); + }); + }, Object.class); + } + +} diff --git a/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/jersey-support/reflect-config.json b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/jersey-support/reflect-config.json new file mode 100644 index 00000000..027b4313 --- /dev/null +++ b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/jersey-support/reflect-config.json @@ -0,0 +1,243 @@ +[ +{ + "name":"java.awt.image.RenderedImage" +}, +{ + "name":"javax.inject.Named", + "allDeclaredMethods":true +}, +{ + "name":"javax.inject.Singleton", + "allDeclaredMethods":true +}, +{ + "name":"javax.naming.InitialContext", + "methods":[ + {"name":"","parameterTypes":[] }, + {"name":"lookup","parameterTypes":["java.lang.String"] } + ] +}, +{ + "name":"javax.xml.transform.Source" +}, +{ + "name":"javax.xml.transform.dom.DOMSource" +}, +{ + "name":"javax.xml.transform.sax.SAXSource" +}, +{ + "name":"javax.xml.transform.stream.StreamSource" +}, +{ + "name":"org.glassfish.hk2.internal.PerThreadContext", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.client.ChunkedInputReader", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.client.ClientAsyncExecutor", + "allDeclaredMethods":true +}, +{ + "name":"org.glassfish.jersey.client.ClientBackgroundScheduler", + "allDeclaredMethods":true +}, +{ + "name":"org.glassfish.jersey.client.JerseyClientBuilder", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.glassfish.jersey.inject.hk2.ContextInjectionResolverImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.inject.hk2.Hk2InjectionManagerFactory", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.glassfish.jersey.inject.hk2.Hk2RequestScope", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.inject.hk2.InstanceSupplierFactoryBridge", + "methods":[{"name":"provide","parameterTypes":[] }] +}, +{ + "name":"org.glassfish.jersey.inject.hk2.JerseyErrorService", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.inject.hk2.RequestContext", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.internal.RuntimeDelegateImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.glassfish.jersey.internal.config.ExternalPropertiesAutoDiscoverable", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.internal.config.ExternalPropertiesConfigurationFeature", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.internal.inject.Custom", + "allDeclaredMethods":true +}, +{ + "name":"org.glassfish.jersey.jackson.JacksonFeature", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.jackson.internal.JacksonAutoDiscoverable", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.logging.LoggingFeatureAutoDiscoverable", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.AbstractFormProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true +}, +{ + "name":"org.glassfish.jersey.message.internal.AbstractMessageReaderWriterProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true +}, +{ + "name":"org.glassfish.jersey.message.internal.BasicTypesMessageProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.ByteArrayProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.DataSourceProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.FileProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.FormMultivaluedMapProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.FormProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.InputStreamProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.ReaderProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.RenderedImageProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.SourceProvider$DomSourceReader", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.SourceProvider$SaxSourceReader", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.SourceProvider$SourceWriter", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.SourceProvider$StreamSourceReader", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.StreamingOutputProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.message.internal.StringMessageProvider", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.glassfish.jersey.process.internal.RequestScope", + "allDeclaredFields":true, + "allDeclaredMethods":true +}, +{ + "name":"org.jvnet.hk2.internal.DynamicConfigurationServiceImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +}, +{ + "name":"org.jvnet.hk2.internal.ServiceLocatorRuntimeImpl", + "allDeclaredFields":true, + "allDeclaredMethods":true, + "allDeclaredConstructors":true +} +] diff --git a/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/jersey-support/resource-config.json b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/jersey-support/resource-config.json new file mode 100644 index 00000000..fa9ff0a2 --- /dev/null +++ b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/jersey-support/resource-config.json @@ -0,0 +1,6 @@ +{ + "resources":{ + "includes":[ + {"pattern":"\\QMETA-INF/services/.*\\E"} + ]} +} diff --git a/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/oci-client-support/reflect-config.json b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/oci-client-support/reflect-config.json new file mode 100644 index 00000000..8fcb17c5 --- /dev/null +++ b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/oci-client-support/reflect-config.json @@ -0,0 +1,335 @@ +[ + { + "name": "org.slf4j.impl.StaticLoggerBinder", + "allDeclaredFields": true, + "allDeclaredConstructors": true + }, +{ + "name":"com.fasterxml.jackson.databind.ext.Java7SupportImpl", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DH$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.DSTU4145$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.EC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.ECGOST$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.ElGamal$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.GM$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.GOST$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.IES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.RSA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.asymmetric.X509$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake2b$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Blake2s$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.DSTU7564$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.GOST3411$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Keccak$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.MD5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD160$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.RIPEMD320$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA224$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA384$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SHA512$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.SM3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Skein$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Tiger$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.digest.Whirlpool$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.drbg.DRBG$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.BC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.BCFKS$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.keystore.PKCS12$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.AES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ARC4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ARIA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Blowfish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.CAST5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.CAST6$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Camellia$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.ChaCha$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DES$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DESede$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.DSTU7624$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.GOST28147$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.GOST3412_2015$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Grain128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Grainv1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.HC128$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.HC256$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.IDEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Noekeon$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.OpenSSLPBKDF$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF1$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPBKDF2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.PBEPKCS12$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Poly1305$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC5$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.RC6$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Rijndael$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SCRYPT$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SEED$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SM4$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Salsa20$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Serpent$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Shacal2$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.SipHash$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Skipjack$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.TEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.TLSKDF$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Threefish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.Twofish$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.VMPC$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.VMPCKSA3$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.XSalsa20$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +}, +{ + "name":"org.bouncycastle.jcajce.provider.symmetric.XTEA$Mappings", + "methods":[{"name":"","parameterTypes":[] }] +} +] diff --git a/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/oci-client-support/resource-config.json b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/oci-client-support/resource-config.json new file mode 100644 index 00000000..b46bdb8c --- /dev/null +++ b/experimental-native-image-support/src/main/resources/META-INF/native-image/com.fnproject.fn/nativeimagesupport/oci-client-support/resource-config.json @@ -0,0 +1,6 @@ +{ + "resources":{ + "includes":[ + {"pattern":"\\Qcom/oracle/bmc/sdk.properties\\E"} + ]} +} diff --git a/experimental-native-image-support/src/test/java/com/fnproject/fn/nativeimagesupport/JacksonFeatureTest.java b/experimental-native-image-support/src/test/java/com/fnproject/fn/nativeimagesupport/JacksonFeatureTest.java new file mode 100644 index 00000000..824d9152 --- /dev/null +++ b/experimental-native-image-support/src/test/java/com/fnproject/fn/nativeimagesupport/JacksonFeatureTest.java @@ -0,0 +1,77 @@ +package com.fnproject.fn.nativeimagesupport; + +import java.math.BigInteger; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created on 16/02/2021. + *

+ * (c) 2021 Oracle Corporation + */ +public class JacksonFeatureTest { + + + @Test + public void shouldWalkAnnotations() { + assertThat(JacksonFeature.expandClassesToMarkForReflection(AbstractBase.class)).containsExactly(AbstractBase.class,ConcreteSubType.class); + assertThat(JacksonFeature.expandClassesToMarkForReflection(InterfaceBase.class)).containsExactly(InterfaceImpl.class); + assertThat(JacksonFeature.expandClassesToMarkForReflection(UsesJacksonFeatures.class)).containsExactly(UsesJacksonFeatures.class,UsesJacksonFeatures.Builder.class); + assertThat(JacksonFeature.expandClassesToMarkForReflection(AnnotationsOnFields.class)).containsExactly(AnnotationsOnFields.class,BigInteger.class); + assertThat(JacksonFeature.expandClassesToMarkForReflection(AnnotationsOnMethods.class)).containsExactly(AnnotationsOnMethods.class,BigInteger.class); + } + + + + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY) + @JsonSubTypes({ + @JsonSubTypes.Type(value = ConcreteSubType.class, name = "c1") + }) + public abstract static class AbstractBase { + + } + + public static class ConcreteSubType extends AbstractBase { + int id; + } + + public static class InterfaceImpl implements InterfaceBase { + } + + /** + * Created on 15/02/2021. + *

+ * (c) 2021 Oracle Corporation + */ + @JsonDeserialize(as = InterfaceImpl.class) + public interface InterfaceBase { + } + + + @JsonDeserialize(builder = UsesJacksonFeatures.Builder.class) + public static class UsesJacksonFeatures { + @JsonPOJOBuilder + public static class Builder { + } + } + + public static class AnnotationsOnFields { + @JsonSerialize(as = BigInteger.class) + private int number; + } + + + public static class AnnotationsOnMethods { + @JsonSerialize(as = BigInteger.class) + public int getNumber() { + return 1; + } + } +} diff --git a/flow-api/pom.xml b/flow-api/pom.xml new file mode 100644 index 00000000..81e25e32 --- /dev/null +++ b/flow-api/pom.xml @@ -0,0 +1,79 @@ + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + flow-api + flow-api + + + com.fnproject.fn + api + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.netbeans.tools + sigtest-maven-plugin + + + + check + + + + + src/main/api/snapshot.sigfile + strictcheck + com.fnproject.fn.api.flow + + + + + diff --git a/flow-api/src/main/api/snapshot.sigfile b/flow-api/src/main/api/snapshot.sigfile new file mode 100644 index 00000000..9048991c --- /dev/null +++ b/flow-api/src/main/api/snapshot.sigfile @@ -0,0 +1,343 @@ +#Signature file v4.1 +#Version 1.0.0-SNAPSHOT + +CLSS public abstract interface com.fnproject.fn.api.flow.Flow +innr public final static !enum FlowState +intf java.io.Serializable +meth public <%0 extends java.io.Serializable, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> invokeFunction(java.lang.String,{%%1},java.lang.Class<{%%0}>) +meth public <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,{%%0}) +meth public abstract !varargs com.fnproject.fn.api.flow.FlowFuture anyOf(com.fnproject.fn.api.flow.FlowFuture[]) +meth public abstract !varargs com.fnproject.fn.api.flow.FlowFuture allOf(com.fnproject.fn.api.flow.FlowFuture[]) +meth public abstract <%0 extends java.io.Serializable, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,{%%1},java.lang.Class<{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,{%%0}) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> completedValue({%%0}) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> createFlowFuture() +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> failedFuture(java.lang.Throwable) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> supply(com.fnproject.fn.api.flow.Flows$SerCallable<{%%0}>) +meth public abstract com.fnproject.fn.api.flow.Flow addTerminationHook(com.fnproject.fn.api.flow.Flows$SerConsumer) +meth public abstract com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers,byte[]) +meth public abstract com.fnproject.fn.api.flow.FlowFuture delay(long,java.util.concurrent.TimeUnit) +meth public abstract com.fnproject.fn.api.flow.FlowFuture supply(com.fnproject.fn.api.flow.Flows$SerRunnable) +meth public com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod) +meth public com.fnproject.fn.api.flow.FlowFuture invokeFunction(java.lang.String,com.fnproject.fn.api.flow.HttpMethod,com.fnproject.fn.api.Headers) + +CLSS public final static !enum com.fnproject.fn.api.flow.Flow$FlowState + outer com.fnproject.fn.api.flow.Flow +fld public final static com.fnproject.fn.api.flow.Flow$FlowState CANCELLED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState FAILED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState KILLED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState SUCCEEDED +fld public final static com.fnproject.fn.api.flow.Flow$FlowState UNKNOWN +meth public static com.fnproject.fn.api.flow.Flow$FlowState valueOf(java.lang.String) +meth public static com.fnproject.fn.api.flow.Flow$FlowState[] values() +supr java.lang.Enum + +CLSS public com.fnproject.fn.api.flow.FlowCompletionException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.RuntimeException + +CLSS public abstract interface com.fnproject.fn.api.flow.FlowFuture<%0 extends java.lang.Object> +intf java.io.Serializable +meth public abstract <%0 extends java.lang.Object, %1 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%1}> thenCombine(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerBiFunction) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture thenAcceptBoth(com.fnproject.fn.api.flow.FlowFuture<{%%0}>,com.fnproject.fn.api.flow.Flows$SerBiConsumer<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> applyToEither(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> handle(com.fnproject.fn.api.flow.Flows$SerBiFunction) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> thenApply(com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},{%%0}>) +meth public abstract <%0 extends java.lang.Object> com.fnproject.fn.api.flow.FlowFuture<{%%0}> thenCompose(com.fnproject.fn.api.flow.Flows$SerFunction<{com.fnproject.fn.api.flow.FlowFuture%0},com.fnproject.fn.api.flow.FlowFuture<{%%0}>>) +meth public abstract boolean cancel() +meth public abstract boolean complete({com.fnproject.fn.api.flow.FlowFuture%0}) +meth public abstract boolean completeExceptionally(java.lang.Throwable) +meth public abstract com.fnproject.fn.api.flow.FlowFuture acceptEither(com.fnproject.fn.api.flow.FlowFuture,com.fnproject.fn.api.flow.Flows$SerConsumer<{com.fnproject.fn.api.flow.FlowFuture%0}>) +meth public abstract com.fnproject.fn.api.flow.FlowFuture thenAccept(com.fnproject.fn.api.flow.Flows$SerConsumer<{com.fnproject.fn.api.flow.FlowFuture%0}>) +meth public abstract com.fnproject.fn.api.flow.FlowFuture thenRun(com.fnproject.fn.api.flow.Flows$SerRunnable) +meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> exceptionally(com.fnproject.fn.api.flow.Flows$SerFunction) +meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> exceptionallyCompose(com.fnproject.fn.api.flow.Flows$SerFunction>) +meth public abstract com.fnproject.fn.api.flow.FlowFuture<{com.fnproject.fn.api.flow.FlowFuture%0}> whenComplete(com.fnproject.fn.api.flow.Flows$SerBiConsumer<{com.fnproject.fn.api.flow.FlowFuture%0},java.lang.Throwable>) +meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} get() +meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} get(long,java.util.concurrent.TimeUnit) throws java.util.concurrent.TimeoutException +meth public abstract {com.fnproject.fn.api.flow.FlowFuture%0} getNow({com.fnproject.fn.api.flow.FlowFuture%0}) + +CLSS public final com.fnproject.fn.api.flow.Flows +innr public abstract interface static FlowSource +innr public abstract interface static SerBiConsumer +innr public abstract interface static SerBiFunction +innr public abstract interface static SerCallable +innr public abstract interface static SerConsumer +innr public abstract interface static SerFunction +innr public abstract interface static SerRunnable +innr public abstract interface static SerSupplier +meth public static com.fnproject.fn.api.flow.Flow currentFlow() +meth public static com.fnproject.fn.api.flow.Flows$FlowSource getCurrentFlowSource() +meth public static void setCurrentFlowSource(com.fnproject.fn.api.flow.Flows$FlowSource) +supr java.lang.Object +hfds flowSource + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$FlowSource + outer com.fnproject.fn.api.flow.Flows +meth public abstract com.fnproject.fn.api.flow.Flow currentFlow() + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerBiConsumer<%0 extends java.lang.Object, %1 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.BiConsumer<{com.fnproject.fn.api.flow.Flows$SerBiConsumer%0},{com.fnproject.fn.api.flow.Flows$SerBiConsumer%1}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerBiFunction<%0 extends java.lang.Object, %1 extends java.lang.Object, %2 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.BiFunction<{com.fnproject.fn.api.flow.Flows$SerBiFunction%0},{com.fnproject.fn.api.flow.Flows$SerBiFunction%1},{com.fnproject.fn.api.flow.Flows$SerBiFunction%2}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerCallable<%0 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.concurrent.Callable<{com.fnproject.fn.api.flow.Flows$SerCallable%0}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerConsumer<%0 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.Consumer<{com.fnproject.fn.api.flow.Flows$SerConsumer%0}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerFunction<%0 extends java.lang.Object, %1 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.Function<{com.fnproject.fn.api.flow.Flows$SerFunction%0},{com.fnproject.fn.api.flow.Flows$SerFunction%1}> + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerRunnable + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.lang.Runnable + +CLSS public abstract interface static com.fnproject.fn.api.flow.Flows$SerSupplier<%0 extends java.lang.Object> + outer com.fnproject.fn.api.flow.Flows + anno 0 java.lang.FunctionalInterface() +intf java.io.Serializable +intf java.util.function.Supplier<{com.fnproject.fn.api.flow.Flows$SerSupplier%0}> + +CLSS public com.fnproject.fn.api.flow.FunctionInvocationException +cons public init(com.fnproject.fn.api.flow.HttpResponse) +meth public com.fnproject.fn.api.flow.HttpResponse getFunctionResponse() +supr java.lang.RuntimeException +hfds functionResponse + +CLSS public com.fnproject.fn.api.flow.FunctionInvokeFailedException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.FunctionTimeoutException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public final !enum com.fnproject.fn.api.flow.HttpMethod +fld public final static com.fnproject.fn.api.flow.HttpMethod DELETE +fld public final static com.fnproject.fn.api.flow.HttpMethod GET +fld public final static com.fnproject.fn.api.flow.HttpMethod HEAD +fld public final static com.fnproject.fn.api.flow.HttpMethod OPTIONS +fld public final static com.fnproject.fn.api.flow.HttpMethod PATCH +fld public final static com.fnproject.fn.api.flow.HttpMethod POST +fld public final static com.fnproject.fn.api.flow.HttpMethod PUT +meth public java.lang.String toString() +meth public static com.fnproject.fn.api.flow.HttpMethod valueOf(java.lang.String) +meth public static com.fnproject.fn.api.flow.HttpMethod[] values() +supr java.lang.Enum +hfds verb + +CLSS public abstract interface com.fnproject.fn.api.flow.HttpRequest +meth public abstract byte[] getBodyAsBytes() +meth public abstract com.fnproject.fn.api.Headers getHeaders() +meth public abstract com.fnproject.fn.api.flow.HttpMethod getMethod() + +CLSS public abstract interface com.fnproject.fn.api.flow.HttpResponse +meth public abstract byte[] getBodyAsBytes() +meth public abstract com.fnproject.fn.api.Headers getHeaders() +meth public abstract int getStatusCode() + +CLSS public com.fnproject.fn.api.flow.InvalidStageResponseException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.LambdaSerializationException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Exception) +supr com.fnproject.fn.api.flow.FlowCompletionException + +CLSS public com.fnproject.fn.api.flow.PlatformException +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +meth public java.lang.Throwable fillInStackTrace() +supr com.fnproject.fn.api.flow.FlowCompletionException + +CLSS public com.fnproject.fn.api.flow.ResultSerializationException +cons public init(java.lang.String,java.lang.Throwable) +supr com.fnproject.fn.api.flow.FlowCompletionException + +CLSS public com.fnproject.fn.api.flow.StageInvokeFailedException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.StageLostException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public com.fnproject.fn.api.flow.StageTimeoutException +cons public init(java.lang.String) +supr com.fnproject.fn.api.flow.PlatformException + +CLSS public final com.fnproject.fn.api.flow.WrappedFunctionException +cons public init(java.lang.Throwable) +intf java.io.Serializable +meth public java.lang.Class getOriginalExceptionType() +supr java.lang.RuntimeException +hfds originalExceptionType + +CLSS public abstract interface java.io.Serializable + +CLSS public abstract interface java.lang.Comparable<%0 extends java.lang.Object> +meth public abstract int compareTo({java.lang.Comparable%0}) + +CLSS public abstract java.lang.Enum<%0 extends java.lang.Enum<{java.lang.Enum%0}>> +cons protected init(java.lang.String,int) +intf java.io.Serializable +intf java.lang.Comparable<{java.lang.Enum%0}> +meth protected final java.lang.Object clone() throws java.lang.CloneNotSupportedException +meth protected final void finalize() +meth public final boolean equals(java.lang.Object) +meth public final int compareTo({java.lang.Enum%0}) +meth public final int hashCode() +meth public final int ordinal() +meth public final java.lang.Class<{java.lang.Enum%0}> getDeclaringClass() +meth public final java.lang.String name() +meth public java.lang.String toString() +meth public static <%0 extends java.lang.Enum<{%%0}>> {%%0} valueOf(java.lang.Class<{%%0}>,java.lang.String) +supr java.lang.Object +hfds name,ordinal + +CLSS public java.lang.Exception +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.Throwable +hfds serialVersionUID + +CLSS public abstract interface !annotation java.lang.FunctionalInterface + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[TYPE]) +intf java.lang.annotation.Annotation + +CLSS public java.lang.Object +cons public init() +meth protected java.lang.Object clone() throws java.lang.CloneNotSupportedException +meth protected void finalize() throws java.lang.Throwable +meth public boolean equals(java.lang.Object) +meth public final java.lang.Class getClass() +meth public final void notify() +meth public final void notifyAll() +meth public final void wait() throws java.lang.InterruptedException +meth public final void wait(long) throws java.lang.InterruptedException +meth public final void wait(long,int) throws java.lang.InterruptedException +meth public int hashCode() +meth public java.lang.String toString() + +CLSS public abstract interface java.lang.Runnable + anno 0 java.lang.FunctionalInterface() +meth public abstract void run() + +CLSS public java.lang.RuntimeException +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +supr java.lang.Exception +hfds serialVersionUID + +CLSS public java.lang.Throwable +cons protected init(java.lang.String,java.lang.Throwable,boolean,boolean) +cons public init() +cons public init(java.lang.String) +cons public init(java.lang.String,java.lang.Throwable) +cons public init(java.lang.Throwable) +intf java.io.Serializable +meth public final java.lang.Throwable[] getSuppressed() +meth public final void addSuppressed(java.lang.Throwable) +meth public java.lang.StackTraceElement[] getStackTrace() +meth public java.lang.String getLocalizedMessage() +meth public java.lang.String getMessage() +meth public java.lang.String toString() +meth public java.lang.Throwable fillInStackTrace() +meth public java.lang.Throwable getCause() +meth public java.lang.Throwable initCause(java.lang.Throwable) +meth public void printStackTrace() +meth public void printStackTrace(java.io.PrintStream) +meth public void printStackTrace(java.io.PrintWriter) +meth public void setStackTrace(java.lang.StackTraceElement[]) +supr java.lang.Object +hfds CAUSE_CAPTION,EMPTY_THROWABLE_ARRAY,NULL_CAUSE_MESSAGE,SELF_SUPPRESSION_MESSAGE,SUPPRESSED_CAPTION,SUPPRESSED_SENTINEL,UNASSIGNED_STACK,backtrace,cause,detailMessage,serialVersionUID,stackTrace,suppressedExceptions +hcls PrintStreamOrWriter,SentinelHolder,WrappedPrintStream,WrappedPrintWriter + +CLSS public abstract interface java.lang.annotation.Annotation +meth public abstract boolean equals(java.lang.Object) +meth public abstract int hashCode() +meth public abstract java.lang.Class annotationType() +meth public abstract java.lang.String toString() + +CLSS public abstract interface !annotation java.lang.annotation.Documented + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation + +CLSS public abstract interface !annotation java.lang.annotation.Retention + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.annotation.RetentionPolicy value() + +CLSS public abstract interface !annotation java.lang.annotation.Target + anno 0 java.lang.annotation.Documented() + anno 0 java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy value=RUNTIME) + anno 0 java.lang.annotation.Target(java.lang.annotation.ElementType[] value=[ANNOTATION_TYPE]) +intf java.lang.annotation.Annotation +meth public abstract java.lang.annotation.ElementType[] value() + +CLSS public abstract interface java.util.concurrent.Callable<%0 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract {java.util.concurrent.Callable%0} call() throws java.lang.Exception + +CLSS public abstract interface java.util.function.BiConsumer<%0 extends java.lang.Object, %1 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract void accept({java.util.function.BiConsumer%0},{java.util.function.BiConsumer%1}) +meth public java.util.function.BiConsumer<{java.util.function.BiConsumer%0},{java.util.function.BiConsumer%1}> andThen(java.util.function.BiConsumer) + +CLSS public abstract interface java.util.function.BiFunction<%0 extends java.lang.Object, %1 extends java.lang.Object, %2 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public <%0 extends java.lang.Object> java.util.function.BiFunction<{java.util.function.BiFunction%0},{java.util.function.BiFunction%1},{%%0}> andThen(java.util.function.Function) +meth public abstract {java.util.function.BiFunction%2} apply({java.util.function.BiFunction%0},{java.util.function.BiFunction%1}) + +CLSS public abstract interface java.util.function.Consumer<%0 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract void accept({java.util.function.Consumer%0}) +meth public java.util.function.Consumer<{java.util.function.Consumer%0}> andThen(java.util.function.Consumer) + +CLSS public abstract interface java.util.function.Function<%0 extends java.lang.Object, %1 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public <%0 extends java.lang.Object> java.util.function.Function<{%%0},{java.util.function.Function%1}> compose(java.util.function.Function) +meth public <%0 extends java.lang.Object> java.util.function.Function<{java.util.function.Function%0},{%%0}> andThen(java.util.function.Function) +meth public abstract {java.util.function.Function%1} apply({java.util.function.Function%0}) +meth public static <%0 extends java.lang.Object> java.util.function.Function<{%%0},{%%0}> identity() + +CLSS public abstract interface java.util.function.Supplier<%0 extends java.lang.Object> + anno 0 java.lang.FunctionalInterface() +meth public abstract {java.util.function.Supplier%0} get() + diff --git a/api/src/main/java/com/fnproject/fn/api/flow/Flow.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flow.java similarity index 92% rename from api/src/main/java/com/fnproject/fn/api/flow/Flow.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/Flow.java index 22ab2ed8..11cfc9f2 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/Flow.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flow.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; import com.fnproject.fn.api.Headers; @@ -33,7 +49,7 @@ public interface Flow extends Serializable { *

* Function IDs should be of the form "APPID/path/in/app" (without leading slash) where APPID may either be a named application or ".", indicating the appID of the current (calling) function. * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method HTTP method to invoke function * @param headers Headers to add to the HTTP request representing the function invocation * @param data input data to function as a byte array - @@ -45,7 +61,7 @@ public interface Flow extends Serializable { * Invoke a function by ID with headers and an empty body *

* - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method HTTP method to invoke function * @param headers Headers to add to the HTTP request representing the function invocation * @return a future which completes normally if the function succeeded and fails if it fails @@ -60,7 +76,7 @@ default FlowFuture invokeFunction(String functionId, HttpMethod me *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param input The input object to send to the function input * @param responseType The expected response type of the target function * @param The Response type @@ -77,7 +93,7 @@ default FlowFuture invokeFunction(String function *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method the HTTP method to use for this call * @param headers additional HTTP headers to pass to this function - * @param input The input object to send to the function input @@ -98,7 +114,7 @@ default FlowFuture invokeFunction(String function *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param input The input object to send to the function input * @param The Input type of the function * @return a flow future that completes with the result of the function, or an error if the function invocation failed @@ -116,7 +132,7 @@ default FlowFuture invokeFunction(String functionId, U input) *

* This currently only maps to JSON via the default JSON mapper in the FDK * - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method the HTTP method to use for this call * @param headers additional HTTP headers to pass to this function - * @param input The input object to send to the function input @@ -130,7 +146,7 @@ default FlowFuture invokeFunction(String functionId, U input) * Invoke a function by ID with no headers *

* - * @param functionId Function ID of function to invoke - this should have the form APPNAME/FUNCTION_PATH (e.g. "myapp/path/to/function" or "./path/to/function"). + * @param functionId Function ID of function to invoke - this should be the function ID returned by `fn inspect function appName fnName` * @param method HTTP method to invoke function * @return a future which completes normally if the function succeeded and fails if it fails * @see #invokeFunction(String, HttpMethod, Headers, byte[]) diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java similarity index 52% rename from api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java index 3f3e017c..051a0f32 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/FlowCompletionException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java similarity index 97% rename from api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java index da3d5e60..1bbc36cc 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/FlowFuture.java @@ -1,7 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; import java.io.Serializable; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; diff --git a/api/src/main/java/com/fnproject/fn/api/flow/Flows.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flows.java similarity index 76% rename from api/src/main/java/com/fnproject/fn/api/flow/Flows.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/Flows.java index 4df4f51a..14097465 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/Flows.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/Flows.java @@ -1,5 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; + import java.io.Serializable; import java.util.Objects; import java.util.concurrent.Callable; @@ -8,7 +25,9 @@ /** * Fn Flow API entry point class - this provides access to the current {@link Flow} for the current function invocation. */ -public class Flows { +public final class Flows { + private Flows() { + } private static FlowSource flowSource; @@ -17,7 +36,7 @@ public class Flows { * * @return the current supplier of the flow runtime */ - public static FlowSource getCurrentFlowSource() { + public static synchronized FlowSource getCurrentFlowSource() { return flowSource; } @@ -35,7 +54,7 @@ public interface FlowSource { * @return the current flow runtime */ public synchronized static Flow currentFlow() { - Objects.requireNonNull(flowSource, "Flows.flowSource is not set - Flows.currentFlow() should only be called from within a FaaS function invocation"); + Objects.requireNonNull(flowSource, "Flows.flowSource is not set - Flows.currentFlow() is the @FnFeature(FlowFeature.class) annotation set on your function?"); return flowSource.currentFlow(); } diff --git a/api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java similarity index 54% rename from api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java index b3cc78c5..5c8c1e14 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvocationException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; /** diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java new file mode 100644 index 00000000..731fae5d --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionInvokeFailedException.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a function call failed within the fn platform - the function may or may not have been invoked and + * that invocation may or may not have completed. + */ +public class FunctionInvokeFailedException extends PlatformException { + public FunctionInvokeFailedException(String reason) { super(reason); } +} diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java new file mode 100644 index 00000000..b38317a9 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/FunctionTimeoutException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a function execution exceeds its configured timeout. + * + * When this exception is raised the fn server has terminated the container hosting the function. + */ +public class FunctionTimeoutException extends PlatformException { + public FunctionTimeoutException(String reason) { super(reason); } +} diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java new file mode 100644 index 00000000..b5109343 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpMethod.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Enum representing the different HTTP types that can be used to invoke an external function + * + * @see Flow#invokeFunction + */ +public enum HttpMethod { + GET("GET"), + HEAD("HEAD"), + POST("POST"), + PUT("PUT"), + DELETE("DELETE"), + OPTIONS("OPTIONS"), + PATCH("PATCH"); + + private final String verb; + + HttpMethod(String verb) { + this.verb = verb; + } + + @Override + public String toString() { + return this.verb; + } +} diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java new file mode 100644 index 00000000..c7e4b3f1 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpRequest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +import com.fnproject.fn.api.Headers; + +/** + * An abstract HTTP request details (without location) + */ +public interface HttpRequest { + /** + * Return the HTTP method used to supply this value + * + * @return the HTTP method + */ + HttpMethod getMethod(); + + /** + * Return the headers on the HTTP request + * + * @return the headers + */ + Headers getHeaders(); + + /** + * Returns the body of the request as a byte array + * + * @return the function request body + */ + byte[] getBodyAsBytes(); +} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java similarity index 51% rename from api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java index 1df7b86f..f994b9ed 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/HttpResponse.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; import com.fnproject.fn.api.Headers; diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java new file mode 100644 index 00000000..852f6c4f --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/InvalidStageResponseException.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a completion stage responds with an incompatible datum type for its corresponding completion + * graph stage. + */ +public class InvalidStageResponseException extends PlatformException { + public InvalidStageResponseException(String reason) { super(reason); } +} diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java new file mode 100644 index 00000000..6a4af778 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/LambdaSerializationException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a lambda or any referenced objects fail to be serialized. + * The cause will typically be a {@link java.io.NotSerializableException} or other {@link java.io.IOException} detailing what could not be serialized + */ +public class LambdaSerializationException extends FlowCompletionException { + public LambdaSerializationException(String message) { + super(message); + } + + public LambdaSerializationException(String message, Exception e) { + super(message, e); + } +} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java similarity index 53% rename from api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java index cb1932e7..9d90277c 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/PlatformException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; /** diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java new file mode 100644 index 00000000..2eafb9c9 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/ResultSerializationException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a result returned by a completion stage fails to be serialized. + */ +public class ResultSerializationException extends FlowCompletionException { + public ResultSerializationException(String message, Throwable e) { + super(message, e); + } +} diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java new file mode 100644 index 00000000..9bd5b04c --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageInvokeFailedException.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a a completion stage invocation failed within Fn - the stage may or may not have been invoked + * and that invocation may or may not have completed. + */ +public class StageInvokeFailedException extends PlatformException { + public StageInvokeFailedException(String reason) { super(reason); } +} diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java new file mode 100644 index 00000000..2e86d650 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageLostException.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a stage failed after an internal error in the flow server, the stage may or may not have been + * invoked and that invocation may or may not have completed. + */ +public class StageLostException extends PlatformException { + public StageLostException(String reason) { super(reason); } +} diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java new file mode 100644 index 00000000..5f84f7a8 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/StageTimeoutException.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +/** + * Exception thrown when a completion stage function execution exceeds it configured timeout - + * the stage may or may not have completed normally. + * + * When this exception is raised the fn server has terminated the container hosting the function. + */ +public class StageTimeoutException extends PlatformException { + public StageTimeoutException(String reason) { super(reason); } +} diff --git a/api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java similarity index 57% rename from api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java rename to flow-api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java index 065e6ee7..0bd7e7fa 100644 --- a/api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/WrappedFunctionException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.api.flow; import java.io.Serializable; diff --git a/flow-api/src/main/java/com/fnproject/fn/api/flow/package-info.java b/flow-api/src/main/java/com/fnproject/fn/api/flow/package-info.java new file mode 100644 index 00000000..f4a567b1 --- /dev/null +++ b/flow-api/src/main/java/com/fnproject/fn/api/flow/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * SDK for creating and running asynchronous processes from within fn for Java. + */ +package com.fnproject.fn.api.flow; diff --git a/flow-api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java b/flow-api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java new file mode 100644 index 00000000..778ccf1c --- /dev/null +++ b/flow-api/src/test/java/com/fnproject/fn/api/flow/FlowsTest.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.api.flow; + +import org.junit.Test; + +import java.lang.reflect.Modifier; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class FlowsTest { + public FlowsTest() { + } + + /** People shall not be allowed to create subclasses of {@code Flow}: + *

+    * static class MyFlows extends Flows {
+    * }
+    * 
+ */ + @Test + public void dontSubclassFlows() { + assertTrue("Flows is final", Modifier.isFinal(Flows.class.getModifiers())); + assertEquals("No visible constructors", 0, Flows.class.getConstructors().length); + } +} diff --git a/flow-runtime/pom.xml b/flow-runtime/pom.xml new file mode 100644 index 00000000..c6d6681e --- /dev/null +++ b/flow-runtime/pom.xml @@ -0,0 +1,106 @@ + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + flow-runtime + flow-runtime + + + + com.fnproject.fn + api + + + com.fnproject.fn + flow-api + + + + com.fnproject.fn + runtime + + + com.fnproject.fn + testing-junit4 + test + + + org.mockito + mockito-core + test + + + junit + junit + + + org.assertj + assertj-core + test + + + org.apache.httpcomponents + httpclient + test + + + + + + + + maven-dependency-plugin + + + copy-dependencies + package + + copy-dependencies + + + ${project.build.directory}/dependency + runtime + true + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java similarity index 96% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java index eea809a9..886d093c 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/APIModel.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.annotation.JsonProperty; @@ -5,8 +21,8 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonUnwrapped; import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.flow.*; import com.fnproject.fn.api.exception.FunctionInputHandlingException; +import com.fnproject.fn.api.flow.*; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; import org.apache.commons.io.IOUtils; diff --git a/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java new file mode 100644 index 00000000..4adcb02c --- /dev/null +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.flow; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class BlobResponse { + @JsonProperty("blob_id") + public String blobId; + + @JsonProperty("length") + public Long blobLength; + + @JsonProperty("content_type") + public String contentType; +} diff --git a/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java new file mode 100644 index 00000000..9b898adf --- /dev/null +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.flow; + +import java.io.InputStream; +import java.util.function.Function; + +public interface BlobStoreClient { + + + BlobResponse writeBlob(String prefix, byte[] bytes, String contentType); + + T readBlob(String prefix, String blobId, Function reader, String expectedContentType); +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java similarity index 71% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java index 341cc647..721161c9 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CodeLocation.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java similarity index 80% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java index cee4a055..235115a0 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClient.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fnproject.fn.api.Headers; @@ -16,8 +32,8 @@ public interface CompleterClient { /** * create a new flow against the flow service * - * @param functionId - * @return + * @param functionId Id of the function for which flow needs to be created + * @return a FlowId */ FlowId createFlow(String functionId); @@ -33,6 +49,10 @@ public interface CompleterClient { * Compose a function into the tree * The transmitted function is wrapped to convert th ElvisFuture into it's completion iD * + * @param flowId flowId for thenCompose + * @param completionId completionId for thenCompose + * @param fn fn for thenCompose + * @param codeLocation codeLocation for thenCompose * @return a completion ID that completes when the completion returned by the inner function completes */ CompletionId thenCompose(FlowId flowId, CompletionId completionId, Serializable fn, CodeLocation codeLocation); diff --git a/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java new file mode 100644 index 00000000..4fdb118b --- /dev/null +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.flow; + +import java.io.Serializable; + +public interface CompleterClientFactory extends Serializable { + CompleterClient getCompleterClient(); + + BlobStoreClient getBlobStoreClient(); + +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java similarity index 54% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java index c533ec5e..5cc5d0ea 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/CompletionId.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import java.io.Serializable; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java similarity index 57% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java index 5aac5105..6caacaa5 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/DefaultHttpResponse.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fnproject.fn.api.Headers; diff --git a/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java new file mode 100644 index 00000000..42728b8e --- /dev/null +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.flow; + +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; + +/** + * Both an HTTP response and an individual part of a multipart MIME stream are constituted of + * a set of headers together with the body stream. This interface abstracts the access to those parts. + */ +interface EntityReader { + String getHeaderElement(String h, String e); + + Optional getHeaderValue(String header); + + InputStream getContentStream(); + + Map getHeaders(); +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java similarity index 89% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java index 81d12f1a..bbd2e3a9 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowContinuationInvoker.java @@ -1,12 +1,27 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fnproject.fn.api.*; +import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; import com.fnproject.fn.api.flow.PlatformException; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; @@ -32,6 +47,10 @@ public final class FlowContinuationInvoker implements FunctionInvoker { public static final String FLOW_ID_HEADER = "Fnproject-FlowId"; + FlowContinuationInvoker() { + + } + private static class URLCompleterClientFactory implements CompleterClientFactory { private final String completerBaseUrl; private transient CompleterClient completerClient; @@ -45,7 +64,7 @@ private static class URLCompleterClientFactory implements CompleterClientFactory public synchronized CompleterClient getCompleterClient() { if (this.completerClient == null) { this.completerClient = new RemoteFlowApiClient(completerBaseUrl + "/v1", - getBlobStoreClient(), new HttpClient()); + getBlobStoreClient(), new HttpClient()); } return this.completerClient; } @@ -135,7 +154,7 @@ public synchronized Flow currentFlow() { if (matchingDispatchPattern != null) { if (matchingDispatchPattern.numArguments() != invokeStageRequest.args.size()) { - throw new FunctionInputHandlingException("Number of arguments provided (" + invokeStageRequest.args.size() + ") in .InvokeStageRequest does not match the number required by the function type (" + matchingDispatchPattern.numArguments() + ")"); + throw new FunctionInputHandlingException("Number of arguments provided (" + invokeStageRequest.args.size() + ") in .InvokeStageRequest does not match the number required by the function type (" + matchingDispatchPattern.numArguments() + ")"); } } else { throw new FunctionInputHandlingException("No functional interface type matches the supplied continuation class"); @@ -168,7 +187,7 @@ public synchronized Flow currentFlow() { @Override public synchronized Flow currentFlow() { if (runtime == null) { - String functionId = evt.getAppName() + evt.getRoute(); + String functionId = ctx.getRuntimeContext().getFunctionID(); CompleterClientFactory factory = getOrCreateCompleterClientFactory(completerBaseUrl); final FlowId flowId = factory.getCompleterClient().createFlow(functionId); runtime = new RemoteFlow(flowId); @@ -204,9 +223,9 @@ private OutputEvent invokeContinuation(BlobStoreClient blobStoreClient, FlowId f APIModel.Datum datum = APIModel.datumFromJava(flowId, ite.getCause(), blobStoreClient); throw new InternalFunctionInvocationException( - "Error invoking flows lambda", - ite.getCause(), - constructOutputEvent(datum, false) + "Error invoking flows lambda", + ite.getCause(), + constructOutputEvent(datum, false) ); } catch (Exception ex) { throw new PlatformException(ex); @@ -223,27 +242,18 @@ private OutputEvent invokeContinuation(BlobStoreClient blobStoreClient, FlowId f */ final static class ContinuationOutputEvent implements OutputEvent { private final byte[] body; + private static final Headers headers = Headers.emptyHeaders().setHeader(OutputEvent.CONTENT_TYPE_HEADER, "application/json"); private ContinuationOutputEvent(boolean success, byte[] body) { this.body = body; } - /** - * The completer expects a 200 on the output event. - * - * @return - */ @Override - public int getStatusCode() { - return OutputEvent.SUCCESS; + public Status getStatus() { + return Status.Success; } - @Override - public Optional getContentType() { - return Optional.of("application/json"); - } - @Override public void writeToOutput(OutputStream out) throws IOException { out.write(body); @@ -251,7 +261,7 @@ public void writeToOutput(OutputStream out) throws IOException { @Override public Headers getHeaders() { - return Headers.emptyHeaders(); + return headers; } } diff --git a/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFeature.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFeature.java new file mode 100644 index 00000000..42df40c7 --- /dev/null +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFeature.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.flow; + +import com.fnproject.fn.api.FunctionInvoker; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.RuntimeFeature; + +/** + * + * The flow feature enables the Flow Client SDK and runtime behaviour in a Java function in order to use Flow in a function you must add the following to the function class: + * + * + * + * import com.fnproject.fn.api.FnFeature; + * import com.fnproject.fn.runtime.flow.FlowFeature; + * + * {@literal @}FnFeature(FlowFeature.class) + * public class MyFunction { + * + * + * public void myFunction(String input){ + * Flows.currentFlow().... + * + * } + * } + * + * + * + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FlowFeature implements RuntimeFeature { + @Override + public void initialize(RuntimeContext context){ + FunctionInvoker invoker = new FlowContinuationInvoker(); + context.addInvoker(invoker,FunctionInvoker.Phase.PreCall); + } +} diff --git a/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java new file mode 100644 index 00000000..9f7cdacb --- /dev/null +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.flow; + +import com.fnproject.fn.api.flow.FlowFuture; + +/** + * Created on 27/11/2017. + *

+ * (c) 2017 Oracle Corporation + */ +public interface FlowFutureSource { + FlowFuture createFlowFuture(CompletionId completionId); +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java similarity index 55% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java index ced213f0..65481eb9 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowId.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import java.io.Serializable; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java similarity index 75% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java index 66e61456..405af273 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowRuntimeGlobals.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.databind.ObjectMapper; @@ -64,7 +80,7 @@ public static void setCompleterClientFactory(CompleterClientFactory currentClien /** * return the current Fn Flow client factory; * - * @return + * @return a factory of CompleterClient */ public static CompleterClientFactory getCompleterClientFactory() { return completerClientFactory; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java similarity index 90% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java index 99637d3c..557fc336 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/HttpClient.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import org.apache.commons.io.IOUtils; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java similarity index 82% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java index cfb21953..64d1fc60 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/JsonInvoke.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.core.JsonProcessingException; @@ -73,7 +89,7 @@ public static FlowFuture invokeFunction(Flow flow, String func String inputString = getObjectMapper().writeValueAsString(input); Headers newHeaders; if (!headers.get("Content-type").isPresent()) { - newHeaders = headers.withHeader("Content-type", "application/json"); + newHeaders = headers.addHeader("Content-type", "application/json"); } else { newHeaders = headers; } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java similarity index 76% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java index 18f0d644..86762868 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteBlobStoreClient.java @@ -1,6 +1,21 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; import java.io.IOException; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java similarity index 93% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java index 48c2f429..f9535139 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlow.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fnproject.fn.api.Headers; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java similarity index 92% rename from runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java rename to flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java index 60c96a09..39783de8 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java +++ b/flow-runtime/src/main/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClient.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.databind.ObjectMapper; @@ -120,12 +136,10 @@ public CompletionId invokeFunction(FlowId flowId, String functionId, byte[] data httpReq.headers = new ArrayList<>(); - headers.getAll().forEach((k, v) -> { - APIModel.HTTPHeader h = new APIModel.HTTPHeader(); - h.key = k; - h.value = v; - httpReq.headers.add(h); - }); + headers.asMap().forEach((k, vs) -> vs.forEach(v -> httpReq.headers.add(APIModel.HTTPHeader.create(k, v)))); + + Map> headersMap = headers.asMap(); + headersMap.forEach((key, values) -> values.forEach(value -> httpReq.headers.add(APIModel.HTTPHeader.create(key, value)))); } httpReq.method = APIModel.HTTPMethod.fromFlow(method); @@ -153,9 +167,9 @@ public CompletionId completedValue(FlowId flowId, boolean success, Object value, APIModel.CompletionResult completionResult = new APIModel.CompletionResult(); completionResult.successful = success; - if(value instanceof RemoteFlow.RemoteFlowFuture) { + if (value instanceof RemoteFlow.RemoteFlowFuture) { APIModel.StageRefDatum stageRefDatum = new APIModel.StageRefDatum(); - stageRefDatum.stageId = ((RemoteFlow.RemoteFlowFuture)value).id(); + stageRefDatum.stageId = ((RemoteFlow.RemoteFlowFuture) value).id(); completionResult.result = stageRefDatum; } else { APIModel.Datum blobDatum = APIModel.datumFromJava(flowId, value, blobStoreClient); @@ -251,14 +265,14 @@ public Object waitForCompletion(FlowId flowId, CompletionId id, ClassLoader igno long remainingTimeout = Math.max(1, start + msTimeout - lastStart); try (HttpClient.HttpResponse response = - httpClient.execute(prepareGet(apiUrlBase + "/flows/" + flowId.getId() + "/stages/" + id.getId() + "/await?timeout_ms=" + remainingTimeout))) { + httpClient.execute(prepareGet(apiUrlBase + "/flows/" + flowId.getId() + "/stages/" + id.getId() + "/await?timeout_ms=" + remainingTimeout))) { if (response.getStatusCode() == 200) { APIModel.AwaitStageResponse resp = FlowRuntimeGlobals.getObjectMapper().readValue(response.getContentStream(), APIModel.AwaitStageResponse.class); if (resp.result.successful) { return resp.result.toJava(flowId, blobStoreClient, getClass().getClassLoader()); } else { - throw new FlowCompletionException((Throwable)resp.result.toJava(flowId, blobStoreClient, getClass().getClassLoader())); + throw new FlowCompletionException((Throwable) resp.result.toJava(flowId, blobStoreClient, getClass().getClassLoader())); } } else if (response.getStatusCode() == 408) { // do nothing go round again @@ -313,10 +327,10 @@ private static PlatformCommunicationException asError(HttpClient.HttpResponse re try { String body = response.entityAsString(); return new PlatformCommunicationException(String.format("Received unexpected response (%d) from " + - "Flow service: %s", response.getStatusCode(), body == null ? "Empty body" : body)); + "Flow service: %s", response.getStatusCode(), body == null ? "Empty body" : body)); } catch (IOException e) { return new PlatformCommunicationException(String.format("Received unexpected response (%d) from " + - "Flow service. Could not read body.", response.getStatusCode()), e); + "Flow service. Could not read body.", response.getStatusCode()), e); } } @@ -342,7 +356,7 @@ private static byte[] serializeClosure(Object data) { private CompletionId addStageWithClosure(APIModel.CompletionOperation operation, FlowId flowId, Serializable supplier, CodeLocation codeLocation, List deps) { byte[] serialized = serializeClosure(supplier); - BlobResponse blobResponse = blobStoreClient.writeBlob(flowId.getId(), serialized, CONTENT_TYPE_JAVA_OBJECT); + BlobResponse blobResponse = blobStoreClient.writeBlob(flowId.getId(), serialized, CONTENT_TYPE_JAVA_OBJECT); return addStage(operation, APIModel.Blob.fromBlobResponse(blobResponse), deps, flowId, codeLocation); diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java similarity index 74% rename from runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java rename to flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java index 389d4e34..227b507b 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java +++ b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsContinuationInvokerTest.java @@ -1,12 +1,27 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fnproject.fn.api.*; +import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.flow.*; -import com.fnproject.fn.runtime.QueryParametersImpl; import com.fnproject.fn.runtime.ReadOnceInputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; import org.junit.After; import org.junit.Before; @@ -15,10 +30,8 @@ import org.junit.rules.ExpectedException; import java.io.*; -import java.io.IOException; -import java.io.InputStream; -import java.io.Serializable; import java.lang.reflect.Method; +import java.time.Instant; import java.util.*; import static com.fnproject.fn.runtime.flow.FlowContinuationInvoker.FLOW_ID_HEADER; @@ -62,9 +75,9 @@ public void continuationInvokedWhenGraphHeaderPresent() throws IOException, Clas // Given InputEvent event = newRequest() - .withClosure((Flows.SerFunction) (x) -> x * 2) - .withJavaObjectArgs(10) - .asEvent(); + .withClosure((Flows.SerFunction) (x) -> x * 2) + .withJavaObjectArgs(10) + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -84,8 +97,8 @@ public void continuationNotInvokedWhenHeaderMissing() throws IOException, ClassN // Given InputEvent event = new InputEventBuilder() - .withBody("") - .build(); + .withBody("") + .build(); // When FlowContinuationInvoker invoker = new FlowContinuationInvoker(); @@ -102,8 +115,8 @@ public void failsIfArgMissing() throws IOException, ClassNotFoundException { // Given InputEvent event = newRequest() - .withClosure((Flows.SerFunction) (x) -> x * 2) - .asEvent(); + .withClosure((Flows.SerFunction) (x) -> x * 2) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -121,8 +134,8 @@ public void failsIfUnknownClosureType() { thrown.expect(FunctionInputHandlingException.class); // Given InputEvent event = newRequest() - .withClosure(new TestIf()) - .asEvent(); + .withClosure(new TestIf()) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); } @@ -143,25 +156,25 @@ private Tc(Serializable closure, Object result, Object... args) { } Tc[] cases = new Tc[]{ - new Tc((Flows.SerConsumer) (v) -> { - }, null, "hello"), - new Tc((Flows.SerBiFunction) (String::concat), "hello bob", "hello ", "bob"), - new Tc((Flows.SerBiConsumer) (a, b) -> { - }, null, "hello ", "bob"), - new Tc((Flows.SerFunction) (String::toUpperCase), "HELLO BOB", "hello bob"), - new Tc((Flows.SerRunnable) () -> { - }, null), - new Tc((Flows.SerCallable) () -> "hello", "hello"), - new Tc((Flows.SerSupplier) () -> "hello", "hello"), + new Tc((Flows.SerConsumer) (v) -> { + }, null, "hello"), + new Tc((Flows.SerBiFunction) (String::concat), "hello bob", "hello ", "bob"), + new Tc((Flows.SerBiConsumer) (a, b) -> { + }, null, "hello ", "bob"), + new Tc((Flows.SerFunction) (String::toUpperCase), "HELLO BOB", "hello bob"), + new Tc((Flows.SerRunnable) () -> { + }, null), + new Tc((Flows.SerCallable) () -> "hello", "hello"), + new Tc((Flows.SerSupplier) () -> "hello", "hello"), }; for (Tc tc : cases) { InputEvent event = newRequest() - .withClosure(tc.closure) - .withJavaObjectArgs(tc.args) - .asEvent(); + .withClosure(tc.closure) + .withJavaObjectArgs(tc.args) + .asEvent(); Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); assertThat(result).isPresent(); @@ -180,13 +193,13 @@ private Tc(Serializable closure, Object result, Object... args) { public void emptyValueCorrectlySerialized() throws IOException, ClassNotFoundException { // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (x) -> { - if (x != null) { - throw new RuntimeException("Not Null"); - } - }) - .withEmptyDatumArg() - .asEvent(); + .withClosure((Flows.SerConsumer) (x) -> { + if (x != null) { + throw new RuntimeException("Not Null"); + } + }) + .withEmptyDatumArg() + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -202,8 +215,8 @@ public void emptyValueCorrectlySerialized() throws IOException, ClassNotFoundExc public void emptyValueCorrectlyDeSerialized() throws IOException, ClassNotFoundException { // Given InputEvent event = newRequest() - .withClosure((Flows.SerSupplier) () -> null) - .asEvent(); + .withClosure((Flows.SerSupplier) () -> null) + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -221,12 +234,12 @@ public void stageRefCorrectlyDeserialized() throws IOException, ClassNotFoundExc // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer>) (x) -> { - assertThat(x).isNotNull(); - assertThat(((RemoteFlow.RemoteFlowFuture) x).id()).isEqualTo("newStage"); - }) - .withStageRefArg("newStage") - .asEvent(); + .withClosure((Flows.SerConsumer>) (x) -> { + assertThat(x).isNotNull(); + assertThat(((RemoteFlow.RemoteFlowFuture) x).id()).isEqualTo("newStage"); + }) + .withStageRefArg("newStage") + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -245,8 +258,8 @@ public void stageRefCorrectlySerialized() throws IOException, ClassNotFoundExcep // Given InputEvent event = newRequest() - .withClosure((Flows.SerSupplier>) () -> ff) - .asEvent(); + .withClosure((Flows.SerSupplier>) () -> ff) + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -266,11 +279,11 @@ public void stageRefCorrectlySerialized() throws IOException, ClassNotFoundExcep public void setsCurrentStageId() throws IOException, ClassNotFoundException { InputEvent event = newRequest() - .withClosure((Flows.SerRunnable) () -> { - assertThat(FlowRuntimeGlobals.getCurrentCompletionId()).contains(new CompletionId("myStage")); - }) - .withStageId("myStage") - .asEvent(); + .withClosure((Flows.SerRunnable) () -> { + assertThat(FlowRuntimeGlobals.getCurrentCompletionId()).contains(new CompletionId("myStage")); + }) + .withStageId("myStage") + .asEvent(); // When Optional result = invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -287,13 +300,13 @@ public void httpRespToFn() throws Exception { // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (x) -> { - assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); - assertThat(x.getStatusCode()).isEqualTo(201); - assertThat(x.getHeaders().get("Foo")).contains("Bar"); - }) - .withHttpRespArg(201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) - .asEvent(); + .withClosure((Flows.SerConsumer) (x) -> { + assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); + assertThat(x.getStatusCode()).isEqualTo(201); + assertThat(x.getHeaders().get("Foo")).contains("Bar"); + }) + .withHttpRespArg(201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) + .asEvent(); // When @@ -309,14 +322,14 @@ public void httpRespToFnWithError() throws Exception { // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (e) -> { - HttpResponse x = e.getFunctionResponse(); - assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); - assertThat(x.getStatusCode()).isEqualTo(201); - assertThat(x.getHeaders().get("Foo")).contains("Bar"); - }) - .withHttpRespArg(false, 201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) - .asEvent(); + .withClosure((Flows.SerConsumer) (e) -> { + HttpResponse x = e.getFunctionResponse(); + assertThat(x.getBodyAsBytes()).isEqualTo("Hello".getBytes()); + assertThat(x.getStatusCode()).isEqualTo(201); + assertThat(x.getHeaders().get("Foo")).contains("Bar"); + }) + .withHttpRespArg(false, 201, "Hello", APIModel.HTTPHeader.create("Foo", "Bar")) + .asEvent(); // When @@ -341,24 +354,24 @@ class TestCase { } } for (TestCase tc : new TestCase[]{ - new TestCase(APIModel.ErrorType.InvalidStageResponse, InvalidStageResponseException.class), - new TestCase(APIModel.ErrorType.FunctionInvokeFailed, FunctionInvokeFailedException.class), - new TestCase(APIModel.ErrorType.FunctionTimeout, FunctionTimeoutException.class), - new TestCase(APIModel.ErrorType.StageFailed, StageInvokeFailedException.class), - new TestCase(APIModel.ErrorType.StageTimeout, StageTimeoutException.class), - new TestCase(APIModel.ErrorType.StageLost, StageLostException.class) + new TestCase(APIModel.ErrorType.InvalidStageResponse, InvalidStageResponseException.class), + new TestCase(APIModel.ErrorType.FunctionInvokeFailed, FunctionInvokeFailedException.class), + new TestCase(APIModel.ErrorType.FunctionTimeout, FunctionTimeoutException.class), + new TestCase(APIModel.ErrorType.StageFailed, StageInvokeFailedException.class), + new TestCase(APIModel.ErrorType.StageTimeout, StageTimeoutException.class), + new TestCase(APIModel.ErrorType.StageLost, StageLostException.class) }) { Class type = tc.exceptionType; // Given InputEvent event = newRequest() - .withClosure((Flows.SerConsumer) (e) -> { + .withClosure((Flows.SerConsumer) (e) -> { - assertThat(e).hasMessage("My Error"); - assertThat(e).isInstanceOf(type); - }) - .withErrorBody(tc.errorType, "My Error") - .asEvent(); + assertThat(e).hasMessage("My Error"); + assertThat(e).isInstanceOf(type); + }) + .withErrorBody(tc.errorType, "My Error") + .asEvent(); // When @@ -376,8 +389,8 @@ class TestCase { public void functionInvocationExceptionThrownIfStageResultIsNotSerializable() { thrown.expect(ResultSerializationException.class); InputEvent event = newRequest() - .withClosure((Flows.SerSupplier) Object::new) - .asEvent(); + .withClosure((Flows.SerSupplier) Object::new) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -395,10 +408,10 @@ public void functionInvocationExceptionThrownIfStageThrowsException() { InputEvent event = newRequest() - .withClosure((Flows.SerRunnable) () -> { - throw new MyRuntimeException(); - }) - .asEvent(); + .withClosure((Flows.SerRunnable) () -> { + throw new MyRuntimeException(); + }) + .asEvent(); invoker.tryInvoke(new EmptyInvocationContext(), event); @@ -433,7 +446,7 @@ public FlowRequestBuilder withClosure(Serializable closure) { public FlowRequestBuilder withJavaObjectArgs(Object... args) { Arrays.stream(args).forEach((arg) -> - req.args.add(blobStore.withResult(flowId, arg, true))); + req.args.add(blobStore.withResult(flowId, arg, true))); return this; } @@ -449,10 +462,10 @@ public InputEvent asEvent() { System.err.println("Req:" + new String(body)); return new InputEventBuilder() - .withHeader(FLOW_ID_HEADER, flowId) - .withHeader("Content-type", "application/json") - .withBody(new ByteArrayInputStream(body)) - .build(); + .withHeader(FLOW_ID_HEADER, flowId) + .withHeader("Content-type", "application/json") + .withBody(new ByteArrayInputStream(body)) + .build(); } public FlowRequestBuilder withEmptyDatumArg() { @@ -550,43 +563,39 @@ public InputEventBuilder withBody(String body) { return this; } - private Map currentHeaders() { - return new HashMap<>(headers.getAll()); + private Map> currentHeaders() { + return new HashMap<>(headers.asMap()); } - public InputEventBuilder withHeaders(Map headers) { - Map updated = currentHeaders(); - updated.putAll(headers); - this.headers = Headers.fromMap(updated); - return this; - } public InputEventBuilder withHeader(String name, String value) { - Map updated = currentHeaders(); - updated.put(name, value); - this.headers = Headers.fromMap(updated); + this.headers = headers.setHeader(name, value); return this; } private String getHeader(String key) { - for (Map.Entry entry : currentHeaders().entrySet()) { - if (key.equalsIgnoreCase(entry.getKey())) { - return entry.getValue(); - } - } - return null; + return headers.get(key).orElse(null); } public InputEvent build() { return new ReadOnceInputEvent( - "", "", "", "", - body, - headers, new QueryParametersImpl()); + body, + headers, "callID", Instant.now()); } } class EmptyRuntimeContext implements RuntimeContext { + @Override + public String getAppID() { + return "appID"; + } + + @Override + public String getFunctionID() { + return "fnID"; + } + @Override public Optional getInvokeInstance() { return Optional.empty(); @@ -637,6 +646,12 @@ public void setInvoker(FunctionInvoker invoker) { throw new RuntimeException("You can't modify the empty runtime context in the tests, sorry."); } + @Override + public void addInvoker(FunctionInvoker invoker, FunctionInvoker.Phase phase) { + throw new RuntimeException("You can't modify the empty runtime context in the tests, sorry."); + + } + @Override public MethodWrapper getMethod() { return null; @@ -655,5 +670,21 @@ public RuntimeContext getRuntimeContext() { public void addListener(InvocationListener listener) { } + @Override + public Headers getRequestHeaders() { + return Headers.emptyHeaders(); + } + + + @Override + public void addResponseHeader(String key, String value) { + + } + + @Override + public void setResponseHeader(String key, String value, String... vs) { + + } + } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java similarity index 94% rename from runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java rename to flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java index 3063a77e..10deb29f 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java +++ b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/RemoteFlowApiClientTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import com.fasterxml.jackson.databind.ObjectMapper; @@ -6,6 +22,7 @@ import com.fnproject.fn.api.flow.Flows; import com.fnproject.fn.api.flow.HttpMethod; import com.fnproject.fn.runtime.exception.PlatformCommunicationException; +import com.fnproject.fn.runtime.flow.*; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; import org.junit.Before; @@ -21,6 +38,7 @@ import java.io.ObjectOutputStream; import java.util.Collections; import java.util.List; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -129,7 +147,9 @@ public void invokeFunctionWithInvalidFunctionId() throws Exception { thrown.expectMessage("Failed to add stage"); // When - completerClient.invokeFunction(new FlowId(testFlowId), testFunctionId, invokeBody, HttpMethod.POST, Headers.fromMap(Collections.singletonMap("Content-type", contentType)), locationFn()); + Map headersMap = Collections.singletonMap("Content-type", contentType); + Headers headers = Headers.fromMap(headersMap); + completerClient.invokeFunction(new FlowId(testFlowId), testFunctionId, invokeBody, HttpMethod.POST, headers, locationFn()); } @Test diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java similarity index 81% rename from runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java rename to flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java index e3559348..1b537c06 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java +++ b/flow-runtime/src/test/java/com/fnproject/fn/runtime/flow/TestBlobStore.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.flow; import java.io.*; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/FnFlowsFunction.java b/flow-runtime/src/test/java/com/fnproject/fn/testing/flowtestfns/FnFlowsFunction.java similarity index 50% rename from runtime/src/test/java/com/fnproject/fn/runtime/testfns/FnFlowsFunction.java rename to flow-runtime/src/test/java/com/fnproject/fn/testing/flowtestfns/FnFlowsFunction.java index 59cc2de1..2e6cf98a 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/FnFlowsFunction.java +++ b/flow-runtime/src/test/java/com/fnproject/fn/testing/flowtestfns/FnFlowsFunction.java @@ -1,10 +1,29 @@ -package com.fnproject.fn.runtime.testfns; - +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flowtestfns; + +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; import java.io.Serializable; +@FnFeature(FlowFeature.class) public class FnFlowsFunction implements Serializable { public static void usingFlows() { diff --git a/flow-testing/pom.xml b/flow-testing/pom.xml new file mode 100644 index 00000000..1d2bb74c --- /dev/null +++ b/flow-testing/pom.xml @@ -0,0 +1,96 @@ + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + flow-testing + flow-testing + + + com.fnproject.fn + testing-junit4 + + + com.fnproject.fn + flow-api + + + com.fnproject.fn + flow-runtime + + + com.fnproject.fn + runtime + + + junit + junit + compile + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FlowTesting.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FlowTesting.java new file mode 100644 index 00000000..54cc16b0 --- /dev/null +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FlowTesting.java @@ -0,0 +1,253 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.flow.*; +import com.fnproject.fn.runtime.flow.*; +import com.fnproject.fn.testing.*; + +import java.io.PrintStream; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.CompletableFuture; + +/** + * FlowTesting allows you to test Fn Flow functions by emulating the Fn Flow completer in a testing environment. + * + *

+ * * Created on 07/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FlowTesting implements FnTestingRuleFeature { + private Map functionStubs = new HashMap<>(); + private static InMemCompleter completer = null; + private final FnTestingRule rule; + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private FlowTesting(FnTestingRule rule) { + + this.rule = rule; + rule.addSharedClass(InMemCompleter.CompleterInvokeClient.class); + rule.addSharedClass(BlobStoreClient.class); + rule.addSharedClass(BlobResponse.class); + rule.addSharedClass(CompleterClientFactory.class); + rule.addSharedClass(CompleterClient.class); + rule.addSharedClass(CompletionId.class); + rule.addSharedClass(FlowId.class); + rule.addSharedClass(Flow.FlowState.class); + rule.addSharedClass(CodeLocation.class); + rule.addSharedClass(HttpMethod.class); + rule.addSharedClass(com.fnproject.fn.api.flow.HttpRequest.class); + rule.addSharedClass(com.fnproject.fn.api.flow.HttpResponse.class); + rule.addSharedClass(FlowCompletionException.class); + rule.addSharedClass(FunctionInvocationException.class); + rule.addSharedClass(PlatformException.class); + rule.addFeature(this); + } + + /** + * Create a Flow + * + * @param rule + * @return + */ + public static FlowTesting create(FnTestingRule rule) { + Objects.requireNonNull(rule, "rule"); + return new FlowTesting(rule); + } + + @Override + public void prepareTest(ClassLoader functionClassLoader, PrintStream stderr, String cls, String method) { + InMemCompleter.CompleterInvokeClient client = new TestRuleCompleterInvokeClient(functionClassLoader, stderr, cls, method); + InMemCompleter.FnInvokeClient fnInvokeClient = new TestRuleFnInvokeClient(); + + // The following must be a static: otherwise the factory (the lambda) will not be serializable. + completer = new InMemCompleter(client, fnInvokeClient); + + } + + @Override + public void prepareFunctionClassLoader(FnTestingClassLoader cl) { + setCompleterClient(cl, completer); + } + + @Override + public void afterTestComplete() { + completer.awaitTermination(); + } + + + private class TestRuleCompleterInvokeClient implements InMemCompleter.CompleterInvokeClient { + private final ClassLoader functionClassLoader; + private final PrintStream oldSystemErr; + private final String cls; + private final String method; + private final Set pool = new HashSet<>(); + + + private TestRuleCompleterInvokeClient(ClassLoader functionClassLoader, PrintStream oldSystemErr, String cls, String method) { + this.functionClassLoader = functionClassLoader; + this.oldSystemErr = oldSystemErr; + this.cls = cls; + this.method = method; + } + + + @Override + public APIModel.CompletionResult invokeStage(String fnId, FlowId flowId, CompletionId stageId, APIModel.Blob closure, List input) { + // Construct a new ClassLoader hierarchy with a copy of the statics embedded in the runtime. + // Initialise it appropriately. + FnTestingClassLoader fcl = new FnTestingClassLoader(functionClassLoader, rule.getSharedPrefixes()); + + + setCompleterClient(fcl, completer); + + + APIModel.InvokeStageRequest request = new APIModel.InvokeStageRequest(); + request.stageId = stageId.getId(); + request.flowId = flowId.getId(); + request.closure = closure; + request.args = input; + + String inputBody = null; + try { + inputBody = objectMapper.writeValueAsString(request); + } catch (JsonProcessingException e) { + throw new IllegalStateException("Invalid request"); + } + + // oldSystemErr.println("Body\n" + new String(inputBody)); + + InputEvent inputEvent = new FnHttpEventBuilder() + .withBody(inputBody) + .withHeader("Content-Type", "application/json") + .withHeader(FlowContinuationInvoker.FLOW_ID_HEADER, flowId.getId()).buildEvent(); + + Map mutableEnv = new HashMap<>(); + PrintStream functionErr = new PrintStream(oldSystemErr); + + // Do we want to capture IO from continuations on the main log stream? + // System.setOut(functionErr); + // System.setErr(functionErr); + + mutableEnv.putAll(rule.getConfig()); + mutableEnv.putAll(rule.getEventEnv()); + mutableEnv.put("FN_FORMAT", "http-stream"); + List output = new ArrayList<>(); + + + fcl.run( + mutableEnv, + new FnTestingRule.TestCodec(Collections.singletonList(inputEvent), output), + functionErr, + cls + "::" + method); + + FnResult out = output.get(0); + + APIModel.CompletionResult r; + try { + + APIModel.InvokeStageResponse response = objectMapper.readValue(out.getBodyAsBytes(), APIModel.InvokeStageResponse.class); + r = response.result; + + } catch (Exception e) { + oldSystemErr.println("Err\n" + e); + e.printStackTrace(oldSystemErr); + r = APIModel.CompletionResult.failure(APIModel.ErrorDatum.newError(APIModel.ErrorType.UnknownError, "Error reading fn Response:" + e.getMessage())); + } + + if (!r.successful) { + throw new ResultException(r.result); + } + return r; + + } + } + + private void setCompleterClient(FnTestingClassLoader cl, CompleterClientFactory completerClientFactory) { + try { + Class completerGlobals = cl.loadClass(FlowRuntimeGlobals.class.getName()); + completerGlobals.getMethod("setCompleterClientFactory", CompleterClientFactory.class).invoke(completerGlobals, completerClientFactory); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException | IllegalArgumentException e) { + throw new RuntimeException("Something broke in the reflective classloader", e); + } + } + + private interface FnFunctionStub { + com.fnproject.fn.api.flow.HttpResponse stubFunction(HttpMethod method, Headers headers, byte[] body); + } + + + public FnFunctionStubBuilder givenFn(String id) { + return new FnFunctionStubBuilder() { + @Override + public FlowTesting withResult(byte[] result) { + return withAction((body) -> result); + } + + @Override + public FlowTesting withFunctionError() { + return withAction((body) -> { + throw new FunctionError("simulated by testing platform"); + }); + } + + @Override + public FlowTesting withPlatformError() { + return withAction((body) -> { + throw new PlatformError("simulated by testing platform"); + }); + } + + @Override + public FlowTesting withAction(ExternalFunctionAction f) { + functionStubs.put(id, (HttpMethod method, Headers headers, byte[] body) -> { + try { + return new DefaultHttpResponse(200, Headers.emptyHeaders(), f.apply(body)); + } catch (FunctionError functionError) { + return new DefaultHttpResponse(500, Headers.emptyHeaders(), functionError.getMessage().getBytes()); + } catch (PlatformError platformError) { + throw new RuntimeException("Platform Error"); + } + }); + return FlowTesting.this; + } + }; + } + + private class TestRuleFnInvokeClient implements InMemCompleter.FnInvokeClient { + @Override + public CompletableFuture invokeFunction(String fnId, HttpMethod method, Headers headers, byte[] data) { + FnFunctionStub stub = functionStubs.computeIfAbsent(fnId, (k) -> { + throw new IllegalStateException("Function was invoked that had no definition: " + k + " defined functions are " + String.join(",",functionStubs.keySet())); + }); + + try { + return CompletableFuture.completedFuture(stub.stubFunction(method, headers, data)); + } catch (Exception e) { + CompletableFuture respFuture = new CompletableFuture<>(); + respFuture.completeExceptionally(e); + return respFuture; + } + } + } +} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnFunctionStubBuilder.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FnFunctionStubBuilder.java similarity index 61% rename from testing/src/main/java/com/fnproject/fn/testing/FnFunctionStubBuilder.java rename to flow-testing/src/main/java/com/fnproject/fn/testing/flow/FnFunctionStubBuilder.java index 9ed01e45..e0c02719 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/FnFunctionStubBuilder.java +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/FnFunctionStubBuilder.java @@ -1,32 +1,51 @@ -package com.fnproject.fn.testing; +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.testing.FunctionError; +import com.fnproject.fn.testing.PlatformError; /** * A builder for constructing stub external functions */ -public interface FnFunctionStubBuilder { +public interface FnFunctionStubBuilder { /** * Consume the builder and stub the function to return the provided byte array * * @param result A byte array returned by the function - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withResult(byte[] result); + T withResult(byte[] result); /** * Consume the builder and stub the function to throw an error when it is invoked: this simulates a failure of the * called function, e.g. if the external function threw an exception. * - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withFunctionError(); + T withFunctionError(); /** * Consume the builder and stub the function to throw a platform error, this simulates a failure of the Fn Flow * completions platform, and not any error of the user code. * - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withPlatformError(); + T withPlatformError(); /** * Consume the builder and stub the function to perform some action; the action is an implementation of the @@ -37,9 +56,9 @@ public interface FnFunctionStubBuilder { * external state is accessed, a synchronization mechanism should be used. * * @param f an action to apply when this function is invoked - * @return The original testing rule (usually {@link FnTestingRule}. The builder is consumed. + * @return The original testing rule (usually {@link FlowTesting}. The builder is consumed. */ - FnTestingRule withAction(ExternalFunctionAction f); + T withAction(ExternalFunctionAction f); /** * Represents the calling interface of an external function. It takes a byte[] as input, diff --git a/testing/src/main/java/com/fnproject/fn/testing/InMemCompleter.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/InMemCompleter.java similarity index 92% rename from testing/src/main/java/com/fnproject/fn/testing/InMemCompleter.java rename to flow-testing/src/main/java/com/fnproject/fn/testing/flow/InMemCompleter.java index 010b4fa1..b695e959 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/InMemCompleter.java +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/InMemCompleter.java @@ -1,4 +1,20 @@ -package com.fnproject.fn.testing; +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; import com.fnproject.fn.api.Headers; import com.fnproject.fn.api.flow.*; @@ -570,37 +586,44 @@ private Stage addDelayStage(long delay) { private Stage addInvokeFunction(String functionId, HttpMethod method, Headers headers, byte[] data) { return addStage(new Stage(CompletableFuture.completedFuture(Collections.emptyList()), - (n, in) -> in.thenComposeAsync((ignored) -> { + (n, in) -> { + return in.thenComposeAsync((ignored) -> { - CompletionStage respFuture = fnInvokeClient.invokeFunction(functionId, method, headers, data); - return respFuture.thenApply((res) -> { - APIModel.HTTPResp apiResp = new APIModel.HTTPResp(); - apiResp.headers = res.getHeaders().getAll().entrySet() - .stream().map(e -> APIModel.HTTPHeader.create(e.getKey(), e.getValue())).collect(Collectors.toList()); + CompletionStage respFuture = fnInvokeClient.invokeFunction(functionId, method, headers, data); + return respFuture.thenApply((res) -> { + APIModel.HTTPResp apiResp = new APIModel.HTTPResp(); + List callHeaders = new ArrayList<>(); - BlobResponse blobResponse = writeBlob(flowId.getId(), res.getBodyAsBytes(), res.getHeaders().get("Content-type").orElse("application/octet-stream")); + for (Map.Entry> e : res.getHeaders().asMap().entrySet()) { + for (String v : e.getValue()) { + callHeaders.add(APIModel.HTTPHeader.create(e.getKey(), v)); + } + } + apiResp.headers = callHeaders; + BlobResponse blobResponse = writeBlob(flowId.getId(), res.getBodyAsBytes(), res.getHeaders().get("Content-type").orElse("application/octet-stream")); - apiResp.body = APIModel.Blob.fromBlobResponse(blobResponse); - apiResp.statusCode = res.getStatusCode(); + apiResp.body = APIModel.Blob.fromBlobResponse(blobResponse); + apiResp.statusCode = res.getStatusCode(); - APIModel.HTTPRespDatum datum = APIModel.HTTPRespDatum.create(apiResp); + APIModel.HTTPRespDatum datum = APIModel.HTTPRespDatum.create(apiResp); - if (apiResp.statusCode >= 200 && apiResp.statusCode < 400) { - return APIModel.CompletionResult.success(datum); - } else { - throw new ResultException(datum); - } + if (apiResp.statusCode >= 200 && apiResp.statusCode < 400) { + return APIModel.CompletionResult.success(datum); + } else { + throw new ResultException(datum); + } - }).exceptionally(e->{ - if (e.getCause() instanceof ResultException){ - throw (ResultException)e.getCause(); - }else{ - throw new ResultException(APIModel.ErrorDatum.newError(APIModel.ErrorType.FunctionInvokeFailed,e.getMessage())); - } - }); + }).exceptionally(e -> { + if (e.getCause() instanceof ResultException) { + throw (ResultException) e.getCause(); + } else { + throw new ResultException(APIModel.ErrorDatum.newError(APIModel.ErrorType.FunctionInvokeFailed, e.getMessage())); + } + }); - }, faasExecutor) + }, faasExecutor); + } )); } diff --git a/flow-testing/src/main/java/com/fnproject/fn/testing/flow/ResultException.java b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/ResultException.java new file mode 100644 index 00000000..3400ccf4 --- /dev/null +++ b/flow-testing/src/main/java/com/fnproject/fn/testing/flow/ResultException.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.runtime.flow.APIModel; + +class ResultException extends RuntimeException { + private final APIModel.Datum datum; + + ResultException(APIModel.Datum datum) { + this.datum = datum; + } + + APIModel.CompletionResult toResult() { + return APIModel.CompletionResult.failure(datum); + } +} diff --git a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleFlowsTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/FnTestingRuleFlowsTest.java similarity index 72% rename from testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleFlowsTest.java rename to flow-testing/src/test/java/com/fnproject/fn/testing/flow/FnTestingRuleFlowsTest.java index 00849159..61ef8349 100644 --- a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleFlowsTest.java +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/FnTestingRuleFlowsTest.java @@ -1,9 +1,30 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.RuntimeContext; +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.api.*; import com.fnproject.fn.api.flow.*; -import org.junit.*; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.testing.FnTestingRule; +import org.assertj.core.api.Assertions; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; import java.io.Serializable; import java.util.Arrays; @@ -11,14 +32,15 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import static org.assertj.core.api.Assertions.assertThat; public class FnTestingRuleFlowsTest { - private static final int HTTP_OK = 200; @Rule public FnTestingRule fn = FnTestingRule.createDefault(); + private FlowTesting flow = FlowTesting.create(fn); + + @FnFeature(FlowFeature.class) public static class Loop { public static int COUNT = 5; @@ -75,11 +97,10 @@ public void setup() { @Test public void completedValue() { fn.givenEvent().enqueue(); - fn.thenRun(TestFn.class, "completedValue"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); - assertThat(result).isEqualTo(Result.CompletedValue); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.CompletedValue); } @Test @@ -88,8 +109,8 @@ public void supply() { fn.thenRun(TestFn.class, "supply"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Supply); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Supply); } @Test @@ -98,8 +119,8 @@ public void allOf() { fn.thenRun(TestFn.class, "allOf"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.AllOf); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.AllOf); } @@ -109,8 +130,8 @@ public void anyOf() { fn.thenRun(TestFn.class, "anyOf"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.AnyOf); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.AnyOf); } @Test() @@ -121,28 +142,28 @@ public void nestedThenCompose() { fn.thenRun(Loop.class, "repeat"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getOnlyResult().getBodyAsString()) - .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, "hello world"))); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getOnlyResult().getBodyAsString()) + .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, "hello world"))); } @Test public void invokeFunctionWithResult() { fn.givenEvent().enqueue(); - fn.givenFn("user/echo") + flow.givenFn("user/echo") .withResult(Result.InvokeFunctionFixed.name().getBytes()); fn.thenRun(TestFn.class, "invokeFunctionEcho"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.InvokeFunctionFixed); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.InvokeFunctionFixed); } @Test public void invokeJsonFunction() { fn.givenEvent().enqueue(); - fn.givenFn("user/json") + flow.givenFn("user/json") .withAction((ign) -> { if (new String(ign).equals("{\"foo\":\"bar\"}")) { return "{\"foo\":\"baz\"}".getBytes(); @@ -153,20 +174,20 @@ public void invokeJsonFunction() { fn.thenRun(TestFn.class, "invokeJsonFunction"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(1); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(1); } @Test public void invokeFunctionWithFunctionError() { fn.givenEvent().enqueue(); - fn.givenFn("user/error") + flow.givenFn("user/error") .withFunctionError(); fn.thenRun(TestFn.class, "invokeFunctionError"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); isInstanceOfAny(exception, FunctionInvocationException.class); } @@ -176,35 +197,35 @@ public void invokeFunctionWithFailedFuture() { fn.thenRun(TestFn.class, "failedFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); - assertThat(exception).isInstanceOf(RuntimeException.class); - assertThat(exception).hasMessage("failedFuture"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(exception).isInstanceOf(RuntimeException.class); + Assertions.assertThat(exception).hasMessage("failedFuture"); } @Test public void invokeFunctionWithPlatformError() { fn.givenEvent().enqueue(); - fn.givenFn("user/error") + flow.givenFn("user/error") .withPlatformError(); fn.thenRun(TestFn.class, "invokeFunctionError"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); isInstanceOfAny(exception, PlatformException.class); } @Test public void invokeFunctionWithAction() { fn.givenEvent().enqueue(); - fn.givenFn("user/echo") + flow.givenFn("user/echo") .withAction((p) -> p); fn.thenRun(TestFn.class, "invokeFunctionEcho"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.InvokeFunctionEcho); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.InvokeFunctionEcho); } @Test @@ -213,8 +234,8 @@ public void completingExceptionally() { fn.thenRun(TestFn.class, "completeExceptionally"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); } @Test @@ -223,8 +244,8 @@ public void completingExceptionallyWhenErrorIsThrownEarlyInGraph() { fn.thenRun(TestFn.class, "completeExceptionallyEarly"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); } @Test @@ -233,9 +254,9 @@ public void cancelledFutureCompletesExceptionally() { fn.thenRun(TestFn.class, "cancelFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); - assertThat(exception).isInstanceOf(CancellationException.class); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(exception).isInstanceOf(CancellationException.class); } @Test @@ -244,10 +265,10 @@ public void completeFutureExceptionallyWithCustomException() { fn.thenRun(TestFn.class, "completeFutureExceptionally"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.Exceptionally); - assertThat(exception).isInstanceOf(RuntimeException.class); - assertThat(exception.getMessage()).isEqualTo("Custom exception"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.Exceptionally); + Assertions.assertThat(exception).isInstanceOf(RuntimeException.class); + Assertions.assertThat(exception.getMessage()).isEqualTo("Custom exception"); } @Test @@ -256,8 +277,8 @@ public void completedFutureCompletesNormally() { fn.thenRun(TestFn.class, "completeFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.CompletedValue); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.CompletedValue); } @Test @@ -266,8 +287,8 @@ public void uncompletedFutureCanBeCompleted() { fn.thenRun(TestFn.class, "createFlowFuture"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.CompletedValue); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.CompletedValue); } @Test @@ -277,8 +298,8 @@ public void shouldLogMessagesToStdErrToPlatformStdErr() { fn.thenRun(TestFn.class, "logToStdErrInContinuation"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); } @Test @@ -288,8 +309,8 @@ public void shouldLogMessagesToStdOutToPlatformStdErr() { fn.thenRun(TestFn.class, "logToStdOutInContinuation"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getStdErrAsString()).contains("TestFn logging: 1"); } @@ -299,9 +320,9 @@ public void shouldHandleMultipleEventsForFunctionWithoutInput() { fn.thenRun(TestFn.class, "anyOf"); - assertThat(fn.getResults().get(0).getStatus()).isEqualTo(HTTP_OK); - assertThat(fn.getResults().get(1).getStatus()).isEqualTo(HTTP_OK); - assertThat(result).isEqualTo(Result.AnyOf); + Assertions.assertThat(fn.getResults().get(0).getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(fn.getResults().get(1).getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(result).isEqualTo(Result.AnyOf); } @Test @@ -310,8 +331,8 @@ public void exceptionallyComposeHandle() { fn.thenRun(TestFn.class, "exceptionallyComposeHandle"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(2); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(2); } @@ -321,8 +342,8 @@ public void exceptionallyComposePassThru() { fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "exceptionallyComposePassThru"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(1); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(1); } @@ -331,8 +352,8 @@ public void exceptionallyComposeThrowsError() { fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "exceptionallyComposeThrowsError"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(HTTP_OK); - assertThat(count).isEqualTo(1); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(count).isEqualTo(1); } @@ -346,8 +367,8 @@ public void shouldHandleMultipleEventsForFunctionWithInput() { fn.thenRun(Loop.class, "repeat"); for (int i = 0; i < bodies.length; i++) { - assertThat(fn.getResults().get(i).getBodyAsString()) - .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, bodies[i]))); + Assertions.assertThat(fn.getResults().get(i).getBodyAsString()) + .isEqualTo(String.join("", Collections.nCopies(Loop.COUNT, bodies[i]))); } } @@ -356,15 +377,15 @@ public void shouldRunShutdownHooksInTest() { fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "terminationHooks"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); - assertThat(result).isEqualTo(Result.TerminationHookRun); + Assertions.assertThat(result).isEqualTo(Result.TerminationHookRun); } // Due to the alien nature of the stored exception, we supply a helper to assert isInstanceOfAny void isInstanceOfAny(Object o, Class... cs) { - assertThat(o).isNotNull(); + Assertions.assertThat(o).isNotNull(); ClassLoader loader = o.getClass().getClassLoader(); for (Class c : cs) { try { @@ -377,6 +398,7 @@ void isInstanceOfAny(Object o, Class... cs) { Assert.fail("Object " + o + "is not an instance of any of " + Arrays.toString(cs)); } + @FnFeature(FlowFeature.class) public static class TestFn { static Integer TO_ADD = null; @@ -384,6 +406,7 @@ public TestFn(RuntimeContext ctx) { TO_ADD = Integer.parseInt(ctx.getConfigurationByKey("ADD").orElse("-1")); } + public void completedValue() { Flows.currentFlow() .completedValue(Result.CompletedValue).thenAccept((r) -> result = r); @@ -670,9 +693,9 @@ static void reset() { // These members are external to the class under test so as to be visible from the unit tests. // They must be public, since the TestFn class will be instantiated under a separate ClassLoader; // therefore we need broader access than might be anticipated. - public static Result result = null; - public static Throwable exception = null; - public static Integer staticConfig = null; - public static Integer count = 0; + public static volatile Result result = null; + public static volatile Throwable exception = null; + public static volatile Integer staticConfig = null; + public static volatile Integer count = 0; } diff --git a/flow-testing/src/test/java/com/fnproject/fn/testing/flow/IntegrationTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/IntegrationTest.java new file mode 100644 index 00000000..341335dd --- /dev/null +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/IntegrationTest.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.FunctionError; +import com.fnproject.fn.testing.flowtestfns.ExerciseEverything; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; + +public class IntegrationTest { + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + + public FlowTesting flow = FlowTesting.create(fn); + + @Test + public void runIntegrationTests() { + + flow.givenFn("testFunctionNonExistant") + .withFunctionError() + + .givenFn("testFunction") + .withAction((body) -> { + if (new String(body).equals("PASS")) { + return "okay".getBytes(); + } else { + throw new FunctionError("failed as demanded"); + } + }); + + fn + .givenEvent() + .withBody("") // or "1,5,6,32" to select a set of tests individually + .enqueue() + + .thenRun(ExerciseEverything.class, "handleRequest"); + + Assertions.assertThat(fn.getResults().get(0).getBodyAsString()) + .endsWith("Everything worked\n"); + } +} diff --git a/testing/src/test/java/com/fnproject/fn/testing/MultipleEventsTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/MultipleEventsTest.java similarity index 76% rename from testing/src/test/java/com/fnproject/fn/testing/MultipleEventsTest.java rename to flow-testing/src/test/java/com/fnproject/fn/testing/flow/MultipleEventsTest.java index 4622b44b..7109e4a6 100644 --- a/testing/src/test/java/com/fnproject/fn/testing/MultipleEventsTest.java +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/MultipleEventsTest.java @@ -1,22 +1,43 @@ -package com.fnproject.fn.testing; - +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.flow.Flow; import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.testing.FnTestingRule; +import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; import java.util.concurrent.Semaphore; -import static org.assertj.core.api.Assertions.assertThat; - public class MultipleEventsTest { @Rule public FnTestingRule fn = FnTestingRule.createDefault(); + public FlowTesting flow = FlowTesting.create(fn); + public static Semaphore oneGo = null; public static Semaphore twoGo = null; public static boolean success = false; + @FnFeature(FlowFeature.class) public static class TestFn { public void handleRequest(String s) { switch (s) { @@ -105,7 +126,7 @@ public void OverlappingFlowInvocationsShouldWork() { fn.thenRun(TestFn.class, "handleRequest"); - assertThat(success).isTrue(); + Assertions.assertThat(success).isTrue(); } } diff --git a/flow-testing/src/test/java/com/fnproject/fn/testing/flow/WhenCompleteTest.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/WhenCompleteTest.java new file mode 100644 index 00000000..da04b1ca --- /dev/null +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flow/WhenCompleteTest.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flow; + +import com.fnproject.fn.api.FnFeature; +import com.fnproject.fn.api.flow.Flows; +import com.fnproject.fn.runtime.flow.FlowFeature; +import com.fnproject.fn.testing.FnTestingRule; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +public class WhenCompleteTest { + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + private FlowTesting flow = FlowTesting.create(fn); + + public static AtomicInteger cas = new AtomicInteger(0); + + + @FnFeature(FlowFeature.class) + public static class TestFn { + public void handleRequest() { + Flows.currentFlow().completedValue(1) + .whenComplete((v, e) -> WhenCompleteTest.cas.compareAndSet(0, 1)) + .thenRun(() -> WhenCompleteTest.cas.compareAndSet(1, 2)); + } + } + + @Test + public void OverlappingFlowInvocationsShouldWork() { + fn.addSharedClass(WhenCompleteTest.class); + + cas.set(0); + + fn.givenEvent().enqueue(); + + fn.thenRun(TestFn.class, "handleRequest"); + + Assertions.assertThat(cas.get()).isEqualTo(2); + } + +} diff --git a/testing/src/test/java/com/fnproject/fn/testing/ExerciseEverything.java b/flow-testing/src/test/java/com/fnproject/fn/testing/flowtestfns/ExerciseEverything.java similarity index 92% rename from testing/src/test/java/com/fnproject/fn/testing/ExerciseEverything.java rename to flow-testing/src/test/java/com/fnproject/fn/testing/flowtestfns/ExerciseEverything.java index 1d4ce58e..729c18cd 100644 --- a/testing/src/test/java/com/fnproject/fn/testing/ExerciseEverything.java +++ b/flow-testing/src/test/java/com/fnproject/fn/testing/flowtestfns/ExerciseEverything.java @@ -1,9 +1,24 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing.flowtestfns; + +import com.fnproject.fn.api.*; import com.fnproject.fn.api.flow.*; -import com.fnproject.fn.runtime.flow.HttpClient; +import com.fnproject.fn.runtime.flow.FlowFeature; import org.apache.commons.io.IOUtils; import org.apache.commons.io.output.TeeOutputStream; @@ -19,8 +34,10 @@ import java.util.*; import java.util.stream.Collectors; +@FnFeature(FlowFeature.class) public class ExerciseEverything { + private boolean okay = true; private ByteArrayOutputStream bos = new ByteArrayOutputStream(); private PrintStream out = new PrintStream(new TeeOutputStream(System.err, bos)); @@ -131,24 +148,22 @@ public FlowFuture catchBubbledException(Flow fl) { @Test(11) @Test.Catch({FlowCompletionException.class, FunctionInvocationException.class}) public FlowFuture nonexistentExternalEvaluation(Flow fl) { - return fl.invokeFunction("nonexistent/nonexistent", HttpMethod.POST, Headers.emptyHeaders(), new byte[0]); + return fl.invokeFunction("testFunctionNonExistant", HttpMethod.POST, Headers.emptyHeaders(), new byte[0]); } @Test(12) @Test.Expect("okay") - public FlowFuture checkPassingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "PASS".getBytes()) - .thenApply((resp) -> { - return resp.getStatusCode() != 200 ? "failure" : new String(resp.getBodyAsBytes()); - }); + public FlowFuture checkPassingInvocation(Flow fl) { + return fl.invokeFunction("testFunction", HttpMethod.POST, Headers.emptyHeaders(), "PASS".getBytes()) + .thenApply((resp) -> resp.getStatusCode() != 200 ? "failure" : new String(resp.getBodyAsBytes())); } // There is currently no way for a hot function to signal failure in the Fn platform. // This test will only work in default mode. @Test(13) @Test.Catch({FlowCompletionException.class, FunctionInvocationException.class}) - public FlowFuture checkFailingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "FAIL".getBytes()); + public FlowFuture checkFailingInvocation(Flow fl) { + return fl.invokeFunction("testFunction", HttpMethod.POST, Headers.emptyHeaders(), "FAIL".getBytes()); } // This original version captures the RT, which captures the factory, which is not serializable @@ -367,6 +382,11 @@ public FlowFuture exceptionallyComposePropagateError(Flow fl) throws IOE private int id; + private final RuntimeContext runtimeContext; + + public ExerciseEverything(RuntimeContext rtc){ + this.runtimeContext = rtc; + } void fail() { if (!failures.contains(id)) { failures.add(id); diff --git a/fn-events-testing/README.md b/fn-events-testing/README.md new file mode 100644 index 00000000..0bd06d96 --- /dev/null +++ b/fn-events-testing/README.md @@ -0,0 +1,22 @@ +# Testing Fn Events + +You can use `FnEventTesting` to test [README.md](../fn-events/README.md) within your functions. + +Start by importing the `fn-events-testing` library into your function in `test` scope: + +```xml + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + +``` + +## Usage +- OCI API Gateway Function - [README.md](../examples/apigateway-event/README.md) +- OCI Service Connector Hub: Monitoring - [README.md](../examples/connectorhub-monitoring/README.md) +- OCI Service Connector Hub: Logging - [README.md](../examples/connectorhub-logging/README.md) +- OCI Service Connector Hub: Streaming - [README.md](../examples/connectorhub-streaming/README.md) +- OCI Service Connector Hub: Queue - [connectorhub-queue](../examples/connectorhub-queue) +- OCI Notifications - [README.md](../examples/notifications/README.md) diff --git a/fn-events-testing/pom.xml b/fn-events-testing/pom.xml new file mode 100644 index 00000000..ec638ca7 --- /dev/null +++ b/fn-events-testing/pom.xml @@ -0,0 +1,87 @@ + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + fn-events-testing + fn-events-testing + + + + com.fnproject.fn + api + + + com.fnproject.fn + fn-events + + + com.fnproject.fn + runtime + + + com.fnproject.fn + testing-core + + + com.fnproject.fn + testing-junit4 + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + junit + junit + compile + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + diff --git a/fn-events-testing/src/main/java/com/fnproject/events/testing/APIGatewayTestFeature.java b/fn-events-testing/src/main/java/com/fnproject/events/testing/APIGatewayTestFeature.java new file mode 100644 index 00000000..9e51dd68 --- /dev/null +++ b/fn-events-testing/src/main/java/com/fnproject/events/testing/APIGatewayTestFeature.java @@ -0,0 +1,192 @@ +package com.fnproject.events.testing; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.net.URLEncoder; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.testing.FnEventBuilder; +import com.fnproject.fn.testing.FnEventBuilderJUnit4; +import com.fnproject.fn.testing.FnHttpEventBuilder; +import com.fnproject.fn.testing.FnResult; +import com.fnproject.fn.testing.FnTestingClassLoader; +import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.FnTestingRuleFeature; + +public class APIGatewayTestFeature implements FnTestingRuleFeature { + private final FnTestingRule rule; + private final ObjectMapper mapper; + + private APIGatewayTestFeature(FnTestingRule rule) { + this(rule, new ObjectMapper()); + } + + private APIGatewayTestFeature(FnTestingRule rule, ObjectMapper mapper) { + this.rule = rule; + this.mapper = mapper; + } + + public static APIGatewayTestFeature createDefault(FnTestingRule rule) { + APIGatewayTestFeature feature = new APIGatewayTestFeature(rule); + rule.addFeature(feature); + return feature; + } + + @Override + public void prepareTest(ClassLoader functionClassLoader, PrintStream stderr, String cls, String method) { + + } + + @Override + public void prepareFunctionClassLoader(FnTestingClassLoader cl) { + + } + + @Override + public void afterTestComplete() { + + } + + public FnEventBuilderJUnit4 givenEvent(APIGatewayRequestEvent event) throws JsonProcessingException { + return new APIGatewayFnEventBuilder(event); + } + + /* + Unwrap output event to APIGatewayResponseEvent type + */ + public APIGatewayResponseEvent getResult(Class tClass) throws IOException { + FnResult result = rule.getOnlyResult(); + APIGatewayResponseEvent.Builder responseBuilder = + new APIGatewayResponseEvent.Builder(); + + if (result.getBodyAsBytes().length != 0) { + T body = mapper.readValue(result.getBodyAsBytes(), tClass); + responseBuilder + .body(body); + } + + Map> myHeaders = new HashMap<>(); + result.getHeaders().asMap().forEach((key, headerValues) -> { + if (key.startsWith("Fn-Http-H-")) { + String httpKey = key.substring("Fn-Http-H-".length()); + if (!httpKey.isEmpty()) { + myHeaders.put(httpKey, headerValues); + } + } + }); + responseBuilder.headers(Headers.emptyHeaders().setHeaders(myHeaders)); + + if (result.getHeaders().get("Fn-Http-Status").isPresent()) { + int statusCode = Integer.parseInt(result.getHeaders().get("Fn-Http-Status").get()); + responseBuilder.statusCode(statusCode); + } + return responseBuilder.build(); + } + + class APIGatewayFnEventBuilder implements FnEventBuilderJUnit4 { + + FnHttpEventBuilder builder = new FnHttpEventBuilder(); + + APIGatewayFnEventBuilder(APIGatewayRequestEvent requestEvent) throws JsonProcessingException { + withBody(mapper.writeValueAsBytes(requestEvent.getBody())); + if (requestEvent.getMethod() != null) { + withHeader("Fn-Http-Method", requestEvent.getMethod()); + } + + if (requestEvent.getHeaders() != null) { + requestEvent.getHeaders().asMap().forEach((key, headerValues) -> { + key = "Fn-Http-H-" + key; + for (String headerValue : headerValues) { + withHeader(key, headerValue); + } + }); + } + + /* + This wraps the test query parameters object into a format the FunctionInvocationContext.class can consume. + */ + String baseUrl = requestEvent.getRequestUrl() != null ? requestEvent.getRequestUrl() : ""; + Map> params = requestEvent.getQueryParameters() != null + ? requestEvent.getQueryParameters().getAll() + : Collections.emptyMap(); + + String query = params.entrySet().stream() + .flatMap(e -> { + String k = urlEncode(e.getKey()); + List vs = e.getValue(); + if (vs == null || vs.isEmpty()) return Stream.of(k + "="); + return vs.stream().map(v -> k + "=" + urlEncode(v)); + }) + .collect(Collectors.joining("&")); + + withHeader("Fn-Http-Request-Url", query.isEmpty() ? baseUrl : baseUrl + "?" + query); + } + + @Override + public FnEventBuilder withHeader(String key, String value) { + builder.withHeader(key, value); + return this; + } + + @Override + public FnEventBuilder withBody(InputStream body) throws IOException { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(byte[] body) { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(String body) { + builder.withBody(body); + return this; + } + + @Override + public FnTestingRule enqueue() { + rule.addInput(builder.buildEvent()); + + return rule; + } + + @Override + public FnTestingRule enqueue(int n) { + if (n <= 0) { + throw new IllegalArgumentException("Invalid count"); + } + for (int i = 0; i < n; i++) { + enqueue(); + } + return rule; + } + + InputEvent build() { + return builder.buildEvent(); + } + } + + private static String urlEncode(String value) { + try { + return URLEncoder.encode(value, StandardCharsets.UTF_8.displayName()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/fn-events-testing/src/main/java/com/fnproject/events/testing/ConnectorHubTestFeature.java b/fn-events-testing/src/main/java/com/fnproject/events/testing/ConnectorHubTestFeature.java new file mode 100644 index 00000000..93c2a9e4 --- /dev/null +++ b/fn-events-testing/src/main/java/com/fnproject/events/testing/ConnectorHubTestFeature.java @@ -0,0 +1,115 @@ +package com.fnproject.events.testing; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.testing.FnEventBuilder; +import com.fnproject.fn.testing.FnEventBuilderJUnit4; +import com.fnproject.fn.testing.FnHttpEventBuilder; +import com.fnproject.fn.testing.FnTestingClassLoader; +import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.FnTestingRuleFeature; + +public class ConnectorHubTestFeature implements FnTestingRuleFeature { + private final FnTestingRule rule; + private final ObjectMapper mapper; + + private ConnectorHubTestFeature(FnTestingRule rule) { + this(rule, new ObjectMapper()); + } + + private ConnectorHubTestFeature(FnTestingRule rule, ObjectMapper mapper) { + this.rule = rule; + this.mapper = mapper; + } + + public static ConnectorHubTestFeature createDefault(FnTestingRule rule) { + ConnectorHubTestFeature feature = new ConnectorHubTestFeature(rule); + rule.addFeature(feature); + return feature; + } + + + @Override + public void prepareTest(ClassLoader functionClassLoader, PrintStream stderr, String cls, String method) { + } + + @Override + public void prepareFunctionClassLoader(FnTestingClassLoader cl) { + + } + + @Override + public void afterTestComplete() { + } + + public FnEventBuilderJUnit4 givenEvent(ConnectorHubBatch event) throws JsonProcessingException { + return new ConnectorHubRequestEventBuilder(event); + } + + class ConnectorHubRequestEventBuilder implements FnEventBuilderJUnit4 { + + FnHttpEventBuilder builder = new FnHttpEventBuilder(); + + ConnectorHubRequestEventBuilder(ConnectorHubBatch requestEvent) throws JsonProcessingException { + withBody(mapper.writeValueAsBytes(requestEvent.getBatch())); + if (requestEvent.getHeaders() != null) { + requestEvent.getHeaders().asMap().forEach((key, headerValues) -> { + for (String headerValue : headerValues) { + withHeader(key, headerValue); + } + }); + } + } + + @Override + public FnEventBuilder withHeader(String key, String value) { + builder.withHeader(key, value); + return this; + } + + @Override + public FnEventBuilder withBody(InputStream body) throws IOException { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(byte[] body) { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(String body) { + builder.withBody(body); + return this; + } + + @Override + public FnTestingRule enqueue() { + rule.addInput(builder.buildEvent()); + + return rule; + } + + @Override + public FnTestingRule enqueue(int n) { + if (n <= 0) { + throw new IllegalArgumentException("Invalid count"); + } + for (int i = 0; i < n; i++) { + enqueue(); + } + return rule; + } + + InputEvent build() { + return builder.buildEvent(); + } + } +} diff --git a/fn-events-testing/src/main/java/com/fnproject/events/testing/NotificationTestFeature.java b/fn-events-testing/src/main/java/com/fnproject/events/testing/NotificationTestFeature.java new file mode 100644 index 00000000..9f90af9d --- /dev/null +++ b/fn-events-testing/src/main/java/com/fnproject/events/testing/NotificationTestFeature.java @@ -0,0 +1,116 @@ +package com.fnproject.events.testing; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.testing.FnEventBuilder; +import com.fnproject.fn.testing.FnEventBuilderJUnit4; +import com.fnproject.fn.testing.FnHttpEventBuilder; +import com.fnproject.fn.testing.FnTestingClassLoader; +import com.fnproject.fn.testing.FnTestingRule; +import com.fnproject.fn.testing.FnTestingRuleFeature; + +public class NotificationTestFeature implements FnTestingRuleFeature { + private final FnTestingRule rule; + private final ObjectMapper mapper; + + private NotificationTestFeature(FnTestingRule rule) { + this(rule, new ObjectMapper()); + } + + private NotificationTestFeature(FnTestingRule rule, ObjectMapper mapper) { + this.rule = rule; + this.mapper = mapper; + } + + public static NotificationTestFeature createDefault(FnTestingRule rule) { + NotificationTestFeature feature = new NotificationTestFeature(rule); + rule.addFeature(feature); + return feature; + } + + + @Override + public void prepareTest(ClassLoader functionClassLoader, PrintStream stderr, String cls, String method) { + } + + @Override + public void prepareFunctionClassLoader(FnTestingClassLoader cl) { + + } + + @Override + public void afterTestComplete() { + } + + public FnEventBuilderJUnit4 givenEvent(NotificationMessage event) throws JsonProcessingException { + return new NotificationRequestEventBuilder(event); + } + + class NotificationRequestEventBuilder implements FnEventBuilderJUnit4 { + + FnHttpEventBuilder builder = new FnHttpEventBuilder(); + + NotificationRequestEventBuilder(NotificationMessage requestEvent) throws JsonProcessingException { + withBody(mapper.writeValueAsBytes(requestEvent.getContent())); + if (requestEvent.getHeaders() != null) { + requestEvent.getHeaders().asMap().forEach((key, headerValues) -> { + for (String headerValue : headerValues) { + withHeader(key, headerValue); + } + }); + } + } + + @Override + public FnEventBuilder withHeader(String key, String value) { + builder.withHeader(key, value); + return this; + } + + @Override + public FnEventBuilder withBody(InputStream body) throws IOException { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(byte[] body) { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(String body) { + builder.withBody(body); + return this; + } + + @Override + public FnTestingRule enqueue() { + rule.addInput(builder.buildEvent()); + + return rule; + } + + @Override + public FnTestingRule enqueue(int n) { + if (n <= 0) { + throw new IllegalArgumentException("Invalid count"); + } + for (int i = 0; i < n; i++) { + enqueue(); + } + return rule; + } + + InputEvent build() { + return builder.buildEvent(); + } + } +} diff --git a/fn-events-testing/src/test/java/com/fnproject/events/testing/APIGatewayTestFeatureTest.java b/fn-events-testing/src/test/java/com/fnproject/events/testing/APIGatewayTestFeatureTest.java new file mode 100644 index 00000000..8c7a176a --- /dev/null +++ b/fn-events-testing/src/test/java/com/fnproject/events/testing/APIGatewayTestFeatureTest.java @@ -0,0 +1,171 @@ +package com.fnproject.events.testing; + + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; +import com.fnproject.fn.runtime.httpgateway.QueryParametersImpl; +import com.fnproject.fn.testing.FnEventBuilderJUnit4; +import com.fnproject.fn.testing.FnTestingRule; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; + +public class APIGatewayTestFeatureTest { + + @Rule + public final FnTestingRule fn = FnTestingRule.createDefault(); + + APIGatewayTestFeature feature = APIGatewayTestFeature.createDefault(fn); + + public static class Body { + public final String message; + + @JsonCreator + public Body(@JsonProperty("message") String message) { + this.message = message; + } + } + +/* + A minimal function that echoes input and captures request. +*/ + public String handle(HTTPGatewayContext ctx, InputEvent inputEvent) { + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string", e); + } + }); + + ctx.getHeaders().asMap().forEach((key, value1) -> value1.forEach(value -> { + ctx.addResponseHeader(key, value); + })); + + ctx.setStatusCode(200); + + return body; + } + + @Test + public void testRequestStringifyBody() throws Exception { + Body reqBody = new Body("hello"); + APIGatewayRequestEvent req = new APIGatewayRequestEvent(new QueryParametersImpl(), reqBody, "GET", "/v2/employee", Headers.emptyHeaders()); + + APIGatewayTestFeature.APIGatewayFnEventBuilder builder = (APIGatewayTestFeature.APIGatewayFnEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals("{\"message\":\"hello\"}", body); + } + + @Test + public void testRequestUrl() throws Exception { + APIGatewayRequestEvent req = new APIGatewayRequestEvent(new QueryParametersImpl(), null, "GET", "/v2/employee", Headers.emptyHeaders()); + + APIGatewayTestFeature.APIGatewayFnEventBuilder builder = (APIGatewayTestFeature.APIGatewayFnEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + assertEquals(req.getRequestUrl(), inputEvent.getHeaders().get("Fn-Http-Request-Url").get()); + } + + @Test + public void testRequestHeaderMethod() throws Exception { + APIGatewayRequestEvent req = new APIGatewayRequestEvent(new QueryParametersImpl(), null, "GET", "/v2/employee", Headers.emptyHeaders()); + + APIGatewayTestFeature.APIGatewayFnEventBuilder builder = (APIGatewayTestFeature.APIGatewayFnEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + assertEquals(req.getMethod(), inputEvent.getHeaders().get("Fn-Http-Method").get()); + } + + @Test + public void testRequestQueryParameters() throws Exception { + Map> queryParams = new HashMap<>(); + List paramValues = new ArrayList<>(); + paramValues.add("bar"); + paramValues.add("foo"); + queryParams.put("foo", paramValues); + queryParams.put("spaces", Collections.singletonList("this has spaces")); + + APIGatewayRequestEvent req = new APIGatewayRequestEvent(new QueryParametersImpl(queryParams), null, "GET", "/v2/employee", Headers.emptyHeaders()); + + APIGatewayTestFeature.APIGatewayFnEventBuilder builder = (APIGatewayTestFeature.APIGatewayFnEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + assertEquals("/v2/employee?spaces=this+has+spaces&foo=bar&foo=foo", inputEvent.getHeaders().get("Fn-Http-Request-Url").get()); + } + + @Test + public void testReturnObjectResponse() throws Exception { + Body reqBody = new Body("hello"); + APIGatewayRequestEvent req = new APIGatewayRequestEvent(new QueryParametersImpl(), reqBody, "GET", "/v2/employee", Headers.emptyHeaders()); + + FnEventBuilderJUnit4 builder = feature.givenEvent(req); + builder.enqueue(); + + fn.thenRun(APIGatewayTestFeatureTest.class, "handle"); + + APIGatewayResponseEvent resp = feature.getResult(Body.class); + + assertNotNull(resp.getBody()); + assertEquals("hello", resp.getBody().message); + } + + @Test + public void testReturnNullObjectResponse() throws Exception { + APIGatewayRequestEvent req = new APIGatewayRequestEvent(new QueryParametersImpl(), null, "GET", "/v2/employee", Headers.emptyHeaders()); + + FnEventBuilderJUnit4 builder = feature.givenEvent(req); + builder.enqueue(); + + fn.thenRun(APIGatewayTestFeatureTest.class, "handle"); + + APIGatewayResponseEvent resp = feature.getResult(Body.class); + + assertNull(resp.getBody()); + } + + @Test + public void testReturnHeaderResponse() throws Exception { + Headers headers = Headers.emptyHeaders() + .addHeader("Custom", "foo") + .addHeader("Custom", "bar"); + + APIGatewayRequestEvent req = new APIGatewayRequestEvent(new QueryParametersImpl(), null, "GET", "/v2/employee", headers); + + FnEventBuilderJUnit4 builder = feature.givenEvent(req); + builder.enqueue(); + + fn.thenRun(APIGatewayTestFeatureTest.class, "handle"); + + APIGatewayResponseEvent resp = feature.getResult(Body.class); + + assertNotNull(resp.getHeaders()); + assertEquals("foo", resp.getHeaders().getAllValues("Custom").get(0)); + assertEquals("bar", resp.getHeaders().getAllValues("Custom").get(1)); + } + +} \ No newline at end of file diff --git a/fn-events-testing/src/test/java/com/fnproject/events/testing/Animal.java b/fn-events-testing/src/test/java/com/fnproject/events/testing/Animal.java new file mode 100644 index 00000000..4cbe3f2e --- /dev/null +++ b/fn-events-testing/src/test/java/com/fnproject/events/testing/Animal.java @@ -0,0 +1,50 @@ +package com.fnproject.events.testing; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Animal { + private final String name; + private final int age; + + @JsonCreator + public Animal(@JsonProperty("name") String name, + @JsonProperty("age") int age) { + this.name = name; + this.age = age; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Animal animal = (Animal) o; + return age == animal.age && Objects.equals(name, animal.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public String toString() { + return "Animal{" + + "name='" + name + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/fn-events-testing/src/test/java/com/fnproject/events/testing/ConnectorHubTestFeatureTest.java b/fn-events-testing/src/test/java/com/fnproject/events/testing/ConnectorHubTestFeatureTest.java new file mode 100644 index 00000000..358d6720 --- /dev/null +++ b/fn-events-testing/src/test/java/com/fnproject/events/testing/ConnectorHubTestFeatureTest.java @@ -0,0 +1,236 @@ +package com.fnproject.events.testing; + +import static org.junit.Assert.assertEquals; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.Datapoint; +import com.fnproject.events.input.sch.LoggingData; +import com.fnproject.events.input.sch.MetricData; +import com.fnproject.events.input.sch.StreamingData; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.testing.FnTestingRule; +import org.apache.commons.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; + +public class ConnectorHubTestFeatureTest { + + @Rule + public final FnTestingRule fn = FnTestingRule.createDefault(); + + ConnectorHubTestFeature feature = ConnectorHubTestFeature.createDefault(fn); + + @Test + public void testMetricDataBody() throws Exception { + ConnectorHubBatch req = new ConnectorHubBatch<>(Collections.singletonList( + new MetricData( + "ns", + null, + "compartment", + "name", + new HashMap<>(), + new HashMap<>(), + Collections.singletonList(new Datapoint( + new Date(1764860467553L), + Double.parseDouble("1.2"), + null) + ) + ) + ), Headers.emptyHeaders()); + + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals( + "[{\"namespace\":\"ns\",\"resourceGroup\":null,\"compartmentId\":\"compartment\",\"name\":\"name\",\"dimensions\":{},\"metadata\":{},\"datapoints\":[{\"timestamp\":1764860467553,\"value\":1.2,\"count\":null}]}]", + body); + } + + @Test + public void testMetricEmptyList() throws Exception { + ConnectorHubBatch req = new ConnectorHubBatch<>(Collections.emptyList(), Headers.emptyHeaders()); + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals("[]", body); + } + + @Test + public void testLoggingDataBody() throws Exception { + Map data = new HashMap(); + data.put("applicationId", "ocid1.fnapp.oc1.xyz"); + data.put("containerId", "n/a"); + data.put("functionId", "ocid1.fnfunc.oc1.xyz"); + data.put("message", "Received function invocation request"); + data.put("opcRequestId", "/abc/def"); + data.put("requestId", "/def/abc"); + data.put("src", "STDOUT"); + + Map oracle = new HashMap(); + oracle.put("compartmentid", "ocid1.tenancy.oc1.xyz"); + oracle.put("ingestedtime", "2025-10-23T15:45:19.457Z"); + oracle.put("loggroupid", "ocid1.loggroup.oc1.abc"); + oracle.put("logid", "ocid1.log.oc1.abc"); + oracle.put("tenantid", "ocid1.tenancy.oc1.xyz"); + + ConnectorHubBatch req = new ConnectorHubBatch<>(Collections.singletonList( + new LoggingData( + "ecb37864-4396-4302-9575-981644949730", + "log-name", + "1.0", + "schedule", + "com.oraclecloud.functions.application.functioninvoke", + data, + oracle, + new Date(1764860467553L) + ) + ), Headers.emptyHeaders()); + + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals( + "[{\"id\":\"ecb37864-4396-4302-9575-981644949730\",\"source\":\"log-name\",\"specversion\":\"1.0\",\"subject\":\"schedule\",\"type\":\"com.oraclecloud.functions.application.functioninvoke\",\"data\":{\"functionId\":\"ocid1.fnfunc.oc1.xyz\",\"opcRequestId\":\"/abc/def\",\"src\":\"STDOUT\",\"requestId\":\"/def/abc\",\"applicationId\":\"ocid1.fnapp.oc1.xyz\",\"containerId\":\"n/a\",\"message\":\"Received function invocation request\"},\"oracle\":{\"compartmentid\":\"ocid1.tenancy.oc1.xyz\",\"ingestedtime\":\"2025-10-23T15:45:19.457Z\",\"loggroupid\":\"ocid1.loggroup.oc1.abc\",\"tenantid\":\"ocid1.tenancy.oc1.xyz\",\"logid\":\"ocid1.log.oc1.abc\"},\"time\":1764860467553}]", + body); + } + + @Test + public void testLoggingDataEmptyList() throws Exception { + ConnectorHubBatch req = new ConnectorHubBatch<>(Collections.emptyList(), Headers.emptyHeaders()); + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals("[]", body); + } + + @Test + public void testStreamingBody() throws Exception { + Animal animal = new Animal("foo", 4); + + StreamingData source = new StreamingData<>( + "stream-name", + "0", + null, + animal, + "3", + new Date(1764860467553L) + ); + + ConnectorHubBatch> req = new ConnectorHubBatch<>(Collections.singletonList( + source + ), Headers.emptyHeaders()); + + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals( + "[{\"stream\":\"stream-name\",\"partition\":\"0\",\"key\":null,\"value\":{\"name\":\"foo\",\"age\":4},\"offset\":\"3\",\"timestamp\":1764860467553}]", + body); + } + + @Test + public void testStreamingStringBody() throws Exception { + StreamingData source = new StreamingData<>( + "stream-name", + "0", + null, + "a plain string", + "3", + new Date(1764860467553L) + ); + + ConnectorHubBatch> req = new ConnectorHubBatch<>(Collections.singletonList( + source + ), Headers.emptyHeaders()); + + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals( + "[{\"stream\":\"stream-name\",\"partition\":\"0\",\"key\":null,\"value\":\"a plain string\",\"offset\":\"3\",\"timestamp\":1764860467553}]", + body); + } + + @Test + public void testStreamingDataEmptyList() throws Exception { + ConnectorHubBatch> req = new ConnectorHubBatch<>(Collections.emptyList(), Headers.emptyHeaders()); + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals("[]", body); + } + + @Test + public void testHeadersEmptyList() throws Exception { + ConnectorHubBatch> req = new ConnectorHubBatch<>(Collections.emptyList(), Headers.emptyHeaders()); + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + assertEquals(Headers.emptyHeaders(), inputEvent.getHeaders()); + } + + @Test + public void testHeadersList() throws Exception { + Headers headers = Headers.emptyHeaders().addHeader("foo", "bar"); + ConnectorHubBatch> req = new ConnectorHubBatch<>(Collections.emptyList(), headers); + ConnectorHubTestFeature.ConnectorHubRequestEventBuilder builder = (ConnectorHubTestFeature.ConnectorHubRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + assertEquals("bar", inputEvent.getHeaders().get("foo").get()); + } +} \ No newline at end of file diff --git a/fn-events-testing/src/test/java/com/fnproject/events/testing/NotificationTestFeatureTest.java b/fn-events-testing/src/test/java/com/fnproject/events/testing/NotificationTestFeatureTest.java new file mode 100644 index 00000000..569d9dac --- /dev/null +++ b/fn-events-testing/src/test/java/com/fnproject/events/testing/NotificationTestFeatureTest.java @@ -0,0 +1,58 @@ +package com.fnproject.events.testing; + +import static org.junit.Assert.assertEquals; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.testing.FnTestingRule; +import org.apache.commons.io.IOUtils; +import org.junit.Rule; +import org.junit.Test; + +public class NotificationTestFeatureTest { + + @Rule + public final FnTestingRule fn = FnTestingRule.createDefault(); + + NotificationTestFeature feature = NotificationTestFeature.createDefault(fn); + + @Test + public void testObjectBody() throws Exception { + Animal animal = new Animal("foo", 3); + NotificationMessage req = new NotificationMessage<>(animal, Headers.emptyHeaders()); + NotificationTestFeature.NotificationRequestEventBuilder builder = (NotificationTestFeature.NotificationRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + String body = inputEvent.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string"); + } + }); + assertEquals( + "{\"name\":\"foo\",\"age\":3}", + body); + } + + @Test + public void testHeadersEmptyList() throws Exception { + NotificationMessage req = new NotificationMessage<>("", Headers.emptyHeaders()); + NotificationTestFeature.NotificationRequestEventBuilder builder = (NotificationTestFeature.NotificationRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + assertEquals(Headers.emptyHeaders(), inputEvent.getHeaders()); + } + + @Test + public void testHeadersList() throws Exception { + Headers headers = Headers.emptyHeaders().addHeader("foo", "bar"); + NotificationMessage req = new NotificationMessage<>("", headers); + NotificationTestFeature.NotificationRequestEventBuilder builder = (NotificationTestFeature.NotificationRequestEventBuilder) feature.givenEvent(req); + InputEvent inputEvent = builder.build(); + + assertEquals("bar", inputEvent.getHeaders().get("foo").get()); + } +} \ No newline at end of file diff --git a/fn-events/README.md b/fn-events/README.md new file mode 100644 index 00000000..3d343f76 --- /dev/null +++ b/fn-events/README.md @@ -0,0 +1,37 @@ +# Support tools for functions OCI integrations + +This is an optional module that can be added to Functions that resolves some common integrations related to +OCI Services handling oracle functions invocations. + +* OCI API Gateway support + +# Enabling the feature in Function: + +Edit your pom file and add this library as a dependency: + +```xml + + com.fnproject.fn + fn-events + ${fdk.version} + +``` + +Optionally, add the fn-events-testing library: + +```xml + + com.fnproject.fn + fn-events-testing + ${fdk.version} + test + +``` + +## Usage +- OCI API Gateway Function - [README.md](../examples/apigateway-event/README.md) +- OCI Service Connector Hub: Monitoring - [README.md](../examples/connectorhub-monitoring/README.md) +- OCI Service Connector Hub: Logging - [README.md](../examples/connectorhub-logging/README.md) +- OCI Service Connector Hub: Streaming - [README.md](../examples/connectorhub-streaming/README.md) +- OCI Service Connector Hub: Queue - [connectorhub-queue](../examples/connectorhub-queue) +- OCI Notifications - [README.md](../examples/notifications/README.md) diff --git a/fn-events/pom.xml b/fn-events/pom.xml new file mode 100644 index 00000000..6cd6eae2 --- /dev/null +++ b/fn-events/pom.xml @@ -0,0 +1,85 @@ + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + fn-events + fn-events + + + + com.fnproject.fn + api + + + com.fnproject.fn + runtime + + + com.fnproject.fn + testing-core + test + + + com.fnproject.fn + testing-junit4 + test + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + junit + junit + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + diff --git a/fn-events/src/main/java/com/fnproject/events/APIGatewayFunction.java b/fn-events/src/main/java/com/fnproject/events/APIGatewayFunction.java new file mode 100644 index 00000000..92e870dd --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/APIGatewayFunction.java @@ -0,0 +1,18 @@ +package com.fnproject.events; + +import com.fnproject.events.coercion.APIGatewayCoercion; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; + +public abstract class APIGatewayFunction { + + @FnConfiguration + public void configure(RuntimeContext ctx){ + ctx.addInputCoercion(APIGatewayCoercion.instance()); + ctx.addOutputCoercion(APIGatewayCoercion.instance()); + } + + public abstract APIGatewayResponseEvent handler(APIGatewayRequestEvent requestEvent); +} diff --git a/fn-events/src/main/java/com/fnproject/events/ConnectorHubFunction.java b/fn-events/src/main/java/com/fnproject/events/ConnectorHubFunction.java new file mode 100644 index 00000000..dedb4966 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/ConnectorHubFunction.java @@ -0,0 +1,16 @@ +package com.fnproject.events; + +import com.fnproject.events.coercion.ConnectorHubCoercion; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; + +public abstract class ConnectorHubFunction { + + @FnConfiguration + public void configure(RuntimeContext ctx){ + ctx.addInputCoercion(ConnectorHubCoercion.instance()); + } + + public abstract void handler(ConnectorHubBatch connectorHubBatch); +} diff --git a/fn-events/src/main/java/com/fnproject/events/NotificationFunction.java b/fn-events/src/main/java/com/fnproject/events/NotificationFunction.java new file mode 100644 index 00000000..b5c9fb01 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/NotificationFunction.java @@ -0,0 +1,16 @@ +package com.fnproject.events; + +import com.fnproject.events.coercion.NotificationCoercion; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; + +public abstract class NotificationFunction { + + @FnConfiguration + public void configure(RuntimeContext ctx){ + ctx.addInputCoercion(NotificationCoercion.instance()); + } + + public abstract void handler(NotificationMessage notification); +} diff --git a/fn-events/src/main/java/com/fnproject/events/coercion/APIGatewayCoercion.java b/fn-events/src/main/java/com/fnproject/events/coercion/APIGatewayCoercion.java new file mode 100644 index 00000000..f42d8f9e --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/coercion/APIGatewayCoercion.java @@ -0,0 +1,157 @@ +package com.fnproject.events.coercion; + +import static com.fnproject.events.coercion.Util.hasEventFnInHierarchy; +import static com.fnproject.events.coercion.Util.readBodyAsString; +import static com.fnproject.fn.api.OutputEvent.CONTENT_TYPE_HEADER; +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fnproject.events.APIGatewayFunction; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.mapper.APIGatewayRequestEventMapper; +import com.fnproject.events.mapper.ApiGatewayRequestMapper; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.fn.api.InputCoercion; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.OutputCoercion; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.runtime.httpgateway.FunctionHTTPGatewayContext; + + +public class APIGatewayCoercion implements InputCoercion, OutputCoercion { + + private static final APIGatewayCoercion instance = new APIGatewayCoercion(); + static final String OM_KEY = APIGatewayCoercion.class.getCanonicalName() + ".om"; + private final ApiGatewayRequestMapper requestMapper; + + public APIGatewayCoercion(ApiGatewayRequestMapper requestMapper) { + this.requestMapper = requestMapper; + } + + private APIGatewayCoercion() { + requestMapper = new APIGatewayRequestEventMapper(); + } + + public static APIGatewayCoercion instance() { + return instance; + } + + private static ObjectMapper objectMapper(InvocationContext ctx) { + Optional omo = ctx.getRuntimeContext().getAttribute(OM_KEY, ObjectMapper.class); + if (!omo.isPresent()) { + ObjectMapper om = new ObjectMapper(); + + ctx.getRuntimeContext().setAttribute(OM_KEY, om); + return om; + } else { + return omo.get(); + } + } + + @Override + public Optional tryCoerceParam(InvocationContext currentContext, int param, InputEvent input, MethodWrapper method) { + if (hasEventFnInHierarchy(method.getTargetClass().getSuperclass(), APIGatewayFunction.class)) { + FunctionHTTPGatewayContext functionHTTPGatewayContext = new FunctionHTTPGatewayContext(currentContext); + + Type type = method.getTargetMethod().getGenericParameterTypes()[param]; + JavaType javaType = objectMapper(currentContext).constructType(type); + List requestGenerics = javaType.getBindings().getTypeParameters(); + + Object body; + + if (!requestGenerics.isEmpty()) { + JavaType requestGeneric = requestGenerics.get(0); + if (requestGeneric.hasRawClass(String.class)) { + body = readBodyAsString(input); + } else { + ObjectMapper mapper = objectMapper(currentContext); + body = input.consumeBody(is -> { + try { + return mapper.readValue(is, requestGeneric); + } catch (IOException e) { + throw coercionFailed(requestGeneric, e); + } + }); + } + } else { + body = readBodyAsString(input); + } + APIGatewayRequestEvent requestEvent = requestMapper.toApiGatewayRequestEvent(functionHTTPGatewayContext, body); + return Optional.of(requestEvent); + } else { + return Optional.empty(); + } + } + + @Override + public Optional wrapFunctionResult(InvocationContext currentContext, MethodWrapper method, Object value) { + if (!hasEventFnInHierarchy(method.getTargetClass().getSuperclass(), APIGatewayFunction.class) || value == null) { + return Optional.empty(); + } + + FunctionHTTPGatewayContext ctx = new FunctionHTTPGatewayContext(currentContext); + APIGatewayResponseEvent responseEvent = (APIGatewayResponseEvent) value; + + String contentType = null; + + if (responseEvent.getHeaders() != null) { + for (String key : responseEvent.getHeaders().asMap().keySet()) { + ctx.addResponseHeader(key, responseEvent.getHeaders().getAllValues(key)); + } + + Optional userSetContentType = responseEvent.getHeaders().get(CONTENT_TYPE_HEADER); + if (userSetContentType.isPresent()) { + contentType = userSetContentType.get(); + } + } + + if (responseEvent.getStatus() != null) { + ctx.setStatusCode(responseEvent.getStatus()); + } + + Optional body = Optional.ofNullable(responseEvent.getBody()); + + Type gs = method.getTargetClass().getGenericSuperclass(); + if (gs instanceof ParameterizedType) { + Type responseGeneric = ((ParameterizedType) gs).getActualTypeArguments()[1]; // response type param + JavaType responseType = TypeFactory + .defaultInstance() + .constructType(responseGeneric); + + if (responseType.hasRawClass(String.class)) { + if (contentType == null) { + contentType = "text/plain"; + } + return Optional.of(OutputEvent.fromBytes(((String) body.orElse("")).getBytes(), OutputEvent.Status.Success, contentType)); + } + if (contentType == null) { + contentType = "application/json"; + } + + try { + return Optional.of(OutputEvent.fromBytes(objectMapper(currentContext).writeValueAsBytes(body.orElse("")), OutputEvent.Status.Success, contentType)); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to render response to JSON", e); + } + } else { + if (contentType == null) { + contentType = "text/plain"; + } + return Optional.of(OutputEvent.fromBytes(((String) body.orElse("")).getBytes(), OutputEvent.Status.Success, contentType)); + } + + } + + private static RuntimeException coercionFailed(Type paramType, Throwable cause) { + return new RuntimeException("Failed to coerce event to user function parameter type " + paramType, cause); + } + +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/coercion/ConnectorHubCoercion.java b/fn-events/src/main/java/com/fnproject/events/coercion/ConnectorHubCoercion.java new file mode 100644 index 00000000..7581786f --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/coercion/ConnectorHubCoercion.java @@ -0,0 +1,70 @@ +package com.fnproject.events.coercion; + +import static com.fnproject.events.coercion.Util.hasEventFnInHierarchy; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.fn.api.InputCoercion; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; + + +public class ConnectorHubCoercion implements InputCoercion> { + + private static final ConnectorHubCoercion instance = new ConnectorHubCoercion(); + static final String OM_KEY = ConnectorHubCoercion.class.getCanonicalName() + ".om"; + + public static ConnectorHubCoercion instance() { + return instance; + } + + private static ObjectMapper objectMapper(InvocationContext ctx) { + Optional omo = ctx.getRuntimeContext().getAttribute(OM_KEY, ObjectMapper.class); + if (!omo.isPresent()) { + ObjectMapper om = new ObjectMapper(); + + ctx.getRuntimeContext().setAttribute(OM_KEY, om); + return om; + } else { + return omo.get(); + } + } + + @Override + public Optional> tryCoerceParam(InvocationContext currentContext, int param, InputEvent input, MethodWrapper method) { + if (hasEventFnInHierarchy(method.getTargetClass().getSuperclass(), ConnectorHubFunction.class)) { + Type type = method.getTargetMethod().getGenericParameterTypes()[param]; + JavaType javaType = objectMapper(currentContext).constructType(type); + List requestGenerics = javaType.getBindings().getTypeParameters(); + + ObjectMapper mapper = objectMapper(currentContext); + + JavaType elementType = requestGenerics.get(0); + JavaType listType = mapper.getTypeFactory() + .constructCollectionType(List.class, elementType); + + List batchedItems = input.consumeBody(is -> { + try { + return mapper.readValue(is, listType); + } catch (IOException e) { + throw coercionFailed(listType, e); + } + }); + + return Optional.of(new ConnectorHubBatch(batchedItems, currentContext.getRequestHeaders())); + } else { + return Optional.empty(); + } + } + + private static RuntimeException coercionFailed(Type paramType, Throwable cause) { + return new RuntimeException("Failed to coerce event to user function parameter type " + paramType, cause); + } + +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/coercion/NotificationCoercion.java b/fn-events/src/main/java/com/fnproject/events/coercion/NotificationCoercion.java new file mode 100644 index 00000000..dbe01092 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/coercion/NotificationCoercion.java @@ -0,0 +1,71 @@ +package com.fnproject.events.coercion; + +import static com.fnproject.events.coercion.Util.hasEventFnInHierarchy; +import static com.fnproject.events.coercion.Util.readBodyAsString; +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.NotificationFunction; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.fn.api.InputCoercion; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; + + +public class NotificationCoercion implements InputCoercion> { + + private static final NotificationCoercion instance = new NotificationCoercion(); + static final String OM_KEY = NotificationCoercion.class.getCanonicalName() + ".om"; + + public static NotificationCoercion instance() { + return instance; + } + + private static ObjectMapper objectMapper(InvocationContext ctx) { + Optional omo = ctx.getRuntimeContext().getAttribute(OM_KEY, ObjectMapper.class); + if (!omo.isPresent()) { + ObjectMapper om = new ObjectMapper(); + + ctx.getRuntimeContext().setAttribute(OM_KEY, om); + return om; + } else { + return omo.get(); + } + } + + @Override + public Optional> tryCoerceParam(InvocationContext currentContext, int param, InputEvent input, MethodWrapper method) { + if (hasEventFnInHierarchy(method.getTargetClass().getSuperclass(), NotificationFunction.class)) { + Type type = method.getTargetMethod().getGenericParameterTypes()[param]; + JavaType javaType = objectMapper(currentContext).constructType(type); + List requestGenerics = javaType.getBindings().getTypeParameters(); + + JavaType elementType = requestGenerics.get(0); + if (elementType.hasRawClass(String.class)) { + return Optional.of(new NotificationMessage(readBodyAsString(input), currentContext.getRequestHeaders())); + } + + ObjectMapper mapper = objectMapper(currentContext); + Object messageContent = input.consumeBody(is -> { + try { + return mapper.readValue(is, elementType); + } catch (IOException e) { + throw coercionFailed(elementType, e); + } + }); + + return Optional.of(new NotificationMessage<>(messageContent, currentContext.getRequestHeaders())); + } else { + return Optional.empty(); + } + } + + private static RuntimeException coercionFailed(Type paramType, Throwable cause) { + return new RuntimeException("Failed to coerce event to user function parameter type " + paramType, cause); + } + +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/coercion/Util.java b/fn-events/src/main/java/com/fnproject/events/coercion/Util.java new file mode 100644 index 00000000..e9c36f08 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/coercion/Util.java @@ -0,0 +1,25 @@ +package com.fnproject.events.coercion; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import com.fnproject.fn.api.InputEvent; +import org.apache.commons.io.IOUtils; + +public class Util { + public static boolean hasEventFnInHierarchy(Class targetClass, Class eventClass) { + for (Class c = targetClass; c != null; c = c.getSuperclass()) { + if (c == eventClass) return true; + } + return false; + } + + static String readBodyAsString(InputEvent input) { + return input.consumeBody(is -> { + try { + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException("Error reading input as string", e); + } + }); + } +} diff --git a/fn-events/src/main/java/com/fnproject/events/coercion/jackson/Base64ToTypeDeserializer.java b/fn-events/src/main/java/com/fnproject/events/coercion/jackson/Base64ToTypeDeserializer.java new file mode 100644 index 00000000..26b91a5c --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/coercion/jackson/Base64ToTypeDeserializer.java @@ -0,0 +1,116 @@ +package com.fnproject.events.coercion.jackson; + + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class Base64ToTypeDeserializer extends JsonDeserializer implements ContextualDeserializer { + private final JavaType targetType; + + public Base64ToTypeDeserializer() { + this(null); + } + + private Base64ToTypeDeserializer(JavaType targetType) { + this.targetType = targetType; + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + final JavaType t = (targetType != null) ? targetType : ctxt.constructType(Object.class); + final ObjectCodec codec = p.getCodec(); + final ObjectMapper mapper = (codec instanceof ObjectMapper) ? (ObjectMapper) codec : new ObjectMapper(); + + JsonToken tok = p.currentToken(); + if (tok == JsonToken.VALUE_NULL) { + return null; + } + + // If we didn't receive a string, just let Jackson handle it normally (it might already be JSON) + if (tok != JsonToken.VALUE_STRING) { + return codec.readValue(p, t); + } + + // We have a string; attempt base64 decode first + String v = p.getValueAsString(); + if (v == null) { + return null; + } + + try { + byte[] bytes = Base64.getDecoder().decode(v); + + // If T is String, return textual content of decoded bytes + if (t.isTypeOrSubTypeOf(String.class)) { + return new String(bytes, StandardCharsets.UTF_8); + } + + // Otherwise, treat decoded bytes as JSON and map to target type T + try (JsonParser jp = mapper.getFactory().createParser(bytes)) { + return mapper.readValue(jp, t); + } + } catch (IllegalArgumentException notBase64) { + // Not valid base64; fallback behavior + + // If T is String, return the raw string as-is + if (t.isTypeOrSubTypeOf(String.class)) { + return v; + } + + // Try to interpret the string itself as JSON for T + // (common case: string contains JSON payload but wasn't base64-encoded) + try { + return mapper.readValue(v, t); + } catch (IOException cannotParseAsJson) { + // Final fallback: wrap as JSON string and let Jackson coerce if possible (e.g., to primitive/wrapper) + // If not compatible, throw a Jackson-standard exception + try { + return mapper.readValue(mapper.writeValueAsBytes(v), t); + } catch (IOException e) { + return ctxt.reportInputMismatch( + t, + "Cannot coerce non-base64 string value into %s: %s", + t.toString(), e.getMessage() + ); + } + } + } + } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) { + JavaType t = (property != null) ? property.getType() : ctxt.getContextualType(); + if (t == null || t.hasRawClass(Object.class)) { + // On constructor params this often mirrors property.getType(); if still Object, + // try the context’s parent (the bean being created) via getContextualType() + JavaType enclosing = ctxt.getContextualType(); + if (enclosing != null && enclosing.containedTypeCount() > 0) { + t = enclosing.containedType(0); // StreamingData -> T + } + if (t == null || t.hasRawClass(Object.class)) { + t = ctxt.constructType(Object.class); + } + } + + return new Base64ToTypeDeserializer(t); + + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException { + return deserialize(p, ctxt); + } + +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/input/APIGatewayRequestEvent.java b/fn-events/src/main/java/com/fnproject/events/input/APIGatewayRequestEvent.java new file mode 100644 index 00000000..ebfb4ad3 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/input/APIGatewayRequestEvent.java @@ -0,0 +1,62 @@ +package com.fnproject.events.input; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.QueryParameters; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class APIGatewayRequestEvent { + + private final QueryParameters queryParameters; + private final T body; + private final String method; + private final String requestUrl; + private final Headers headers; + + public APIGatewayRequestEvent(QueryParameters queryParameters, T body, String method, String requestUrl, Headers headers) { + this.queryParameters = queryParameters; + this.body = body; + this.method = method; + this.requestUrl = requestUrl; + this.headers = headers; + } + + public T getBody() { + return body; + } + + public QueryParameters getQueryParameters() { + return queryParameters; + } + + public String getMethod() { + return method; + } + + public Headers getHeaders() { + return headers; + } + + public String getRequestUrl() { + return requestUrl; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + APIGatewayRequestEvent that = (APIGatewayRequestEvent) o; + return Objects.equals(queryParameters, that.queryParameters) && Objects.equals(body, that.body) && Objects.equals(method, that.method) && + Objects.equals(requestUrl, that.requestUrl) && Objects.equals(headers, that.headers); + } + + @Override + public int hashCode() { + return Objects.hash(queryParameters, body, method, requestUrl, headers); + } +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/input/ConnectorHubBatch.java b/fn-events/src/main/java/com/fnproject/events/input/ConnectorHubBatch.java new file mode 100644 index 00000000..e677bbd9 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/input/ConnectorHubBatch.java @@ -0,0 +1,41 @@ +package com.fnproject.events.input; + +import java.util.List; +import java.util.Objects; +import com.fnproject.fn.api.Headers; + +public class ConnectorHubBatch { + private final Headers headers; + private final List batch; + + public ConnectorHubBatch(List batch, Headers headers) { + this.batch = batch; + this.headers = headers; + } + + public List getBatch() { + return batch; + } + + public Headers getHeaders() { + return headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ConnectorHubBatch that = (ConnectorHubBatch) o; + return Objects.equals(headers, that.headers) && Objects.equals(batch, that.batch); + } + + @Override + public int hashCode() { + return Objects.hash(headers, batch); + } + +} diff --git a/fn-events/src/main/java/com/fnproject/events/input/NotificationMessage.java b/fn-events/src/main/java/com/fnproject/events/input/NotificationMessage.java new file mode 100644 index 00000000..b0f7272b --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/input/NotificationMessage.java @@ -0,0 +1,40 @@ +package com.fnproject.events.input; + +import java.util.Objects; +import com.fnproject.fn.api.Headers; + +public class NotificationMessage { + private final T content; + private final Headers headers; + + public NotificationMessage(T content, Headers headers) { + this.content = content; + this.headers = headers; + } + + public T getContent() { + return content; + } + + public Headers getHeaders() { + return headers; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NotificationMessage message = (NotificationMessage) o; + return Objects.equals(content, message.content) && Objects.equals(headers, message.headers); + } + + @Override + public int hashCode() { + return Objects.hash(content, headers); + } + +} diff --git a/fn-events/src/main/java/com/fnproject/events/input/sch/Datapoint.java b/fn-events/src/main/java/com/fnproject/events/input/sch/Datapoint.java new file mode 100644 index 00000000..704e02ec --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/input/sch/Datapoint.java @@ -0,0 +1,63 @@ +package com.fnproject.events.input.sch; + +import java.util.Date; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public final class Datapoint { + private final Date timestamp; + private final Double value; + private final Integer count; + + @JsonCreator + public Datapoint( + @JsonProperty("timestamp") Date timestamp, + @JsonProperty("value") Double value, + @JsonProperty("count") Integer count) { + this.timestamp = timestamp; + this.value = value; + this.count = count; + } + + public Date getTimestamp() { + return timestamp; + } + + public Double getValue() { + return value; + } + + public Integer getCount() { + return count; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Datapoint datapoint = (Datapoint) o; + return Objects.equals(timestamp, datapoint.timestamp) && Objects.equals(value, datapoint.value) && Objects.equals(count, datapoint.count); + } + + @Override + public int hashCode() { + return Objects.hash(timestamp, value, count); + } + + @Override + public String toString() { + return "Datapoint{" + + "timestamp=" + timestamp + + ", value=" + value + + ", count=" + count + + '}'; + } + +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/input/sch/LoggingData.java b/fn-events/src/main/java/com/fnproject/events/input/sch/LoggingData.java new file mode 100644 index 00000000..b8db16af --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/input/sch/LoggingData.java @@ -0,0 +1,106 @@ +package com.fnproject.events.input.sch; + +import java.util.Date; +import java.util.Map; +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class LoggingData { + private final String id; + private final String source; + private final String specversion; + private final String subject; + private final String type; + private final Map data; + private final Map oracle; + private final Date time; + + @JsonCreator + public LoggingData( + @JsonProperty("id") String id, + @JsonProperty("source") String source, + @JsonProperty("specversion") String specversion, + @JsonProperty("subject") String subject, + @JsonProperty("type") String type, + @JsonProperty("data") Map data, + @JsonProperty("oracle") Map oracle, + @JsonProperty("time") Date time) { + + this.id = id; + this.source = source; + this.specversion = specversion; + this.subject = subject; + this.type = type; + this.data = data; + this.oracle = oracle; + this.time = time; + } + + public String getId() { + return id; + } + + public String getSource() { + return source; + } + + public String getSpecversion() { + return specversion; + } + + public String getSubject() { + return subject; + } + + public Map getData() { + return data; + } + + public Map getOracle() { + return oracle; + } + + public Date getTime() { + return time; + } + + public String getType() { + return type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + LoggingData that = (LoggingData) o; + return Objects.equals(id, that.id) && Objects.equals(source, that.source) && Objects.equals(specversion, that.specversion) && + Objects.equals(subject, that.subject) && Objects.equals(type, that.type) && Objects.equals(data, that.data) && + Objects.equals(oracle, that.oracle) && Objects.equals(time, that.time); + } + + @Override + public int hashCode() { + return Objects.hash(id, source, specversion, subject, type, data, oracle, time); + } + + @Override + public String toString() { + return "LoggingData{" + + "id='" + id + '\'' + + ", source='" + source + '\'' + + ", specversion='" + specversion + '\'' + + ", subject='" + subject + '\'' + + ", type='" + type + '\'' + + ", data=" + data + + ", oracle=" + oracle + + ", time=" + time + + '}'; + } +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/input/sch/MetricData.java b/fn-events/src/main/java/com/fnproject/events/input/sch/MetricData.java new file mode 100644 index 00000000..d427ecab --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/input/sch/MetricData.java @@ -0,0 +1,81 @@ +package com.fnproject.events.input.sch; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class MetricData { + private final String namespace; + private final String resourceGroup; + private final String compartmentId; + private final String name; + private final Map dimensions; + private final Map metadata; + private final List datapoints; + + @JsonCreator + public MetricData( + @JsonProperty("namespace") String namespace, + @JsonProperty("resourceGroup") String resourceGroup, + @JsonProperty("compartmentId") String compartmentId, + @JsonProperty("name") String name, + @JsonProperty("dimensions") Map dimensions, + @JsonProperty("metadata") Map metadata, + @JsonProperty("datapoints") List datapoints) { + + this.namespace = namespace; + this.resourceGroup = resourceGroup; + this.compartmentId = compartmentId; + this.name = name; + this.dimensions = dimensions; + this.metadata = metadata; + this.datapoints = datapoints; + } + + public String getNamespace() { + return namespace; + } + + public String getResourceGroup() { + return resourceGroup; + } + + public String getCompartmentId() { + return compartmentId; + } + + public String getName() { + return name; + } + + public Map getDimensions() { + return dimensions; + } + + public Map getMetadata() { + return metadata; + } + + public List getDatapoints() { + return datapoints; + } + + + @Override + public String toString() { + return "MetricData{" + + "namespace='" + namespace + '\'' + + ", resourceGroup='" + resourceGroup + '\'' + + ", compartmentId='" + compartmentId + '\'' + + ", name='" + name + '\'' + + ", dimensions=" + dimensions + + ", metadata=" + metadata + + ", datapoints=" + datapoints + + '}'; + } + +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/input/sch/StreamingData.java b/fn-events/src/main/java/com/fnproject/events/input/sch/StreamingData.java new file mode 100644 index 00000000..b93ee640 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/input/sch/StreamingData.java @@ -0,0 +1,90 @@ +package com.fnproject.events.input.sch; + +import java.util.Date; +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fnproject.events.coercion.jackson.Base64ToTypeDeserializer; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class StreamingData { + private final String stream; + private final String partition; + private final String key; + private final T value; + private final String offset; + private final Date timestamp; + + @JsonCreator + public StreamingData( + @JsonProperty("stream") String stream, + @JsonProperty("partition") String partition, + @JsonProperty("key") String key, + @JsonDeserialize(using = Base64ToTypeDeserializer.class) @JsonProperty("value") T value, + @JsonProperty("offset") String offset, + @JsonProperty("time") Date timestamp) { + this.stream = stream; + this.partition = partition; + this.key = key; + this.value = value; + this.offset = offset; + this.timestamp = timestamp; + } + + public String getStream() { + return stream; + } + + public String getPartition() { + return partition; + } + + public String getKey() { + return key; + } + + public T getValue() { + return value; + } + + public Date getTimestamp() { + return timestamp; + } + + public String getOffset() { + return offset; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + StreamingData that = (StreamingData) o; + return Objects.equals(stream, that.stream) && Objects.equals(partition, that.partition) && Objects.equals(key, that.key) && + Objects.equals(value, that.value) && Objects.equals(offset, that.offset) && Objects.equals(timestamp, that.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(stream, partition, key, value, offset, timestamp); + } + + @Override + public String toString() { + return "StreamingData{" + + "stream='" + stream + '\'' + + ", partition='" + partition + '\'' + + ", key='" + key + '\'' + + ", value=" + value + + ", offset='" + offset + '\'' + + ", timestamp=" + timestamp + + '}'; + } +} \ No newline at end of file diff --git a/fn-events/src/main/java/com/fnproject/events/mapper/APIGatewayRequestEventMapper.java b/fn-events/src/main/java/com/fnproject/events/mapper/APIGatewayRequestEventMapper.java new file mode 100644 index 00000000..d7fb4f41 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/mapper/APIGatewayRequestEventMapper.java @@ -0,0 +1,19 @@ +package com.fnproject.events.mapper; + +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.QueryParameters; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; +import com.fnproject.fn.runtime.httpgateway.QueryParametersImpl; + +public class APIGatewayRequestEventMapper implements ApiGatewayRequestMapper { + + public APIGatewayRequestEvent toApiGatewayRequestEvent(HTTPGatewayContext context, T body) { + QueryParameters queryParameters = + context.getQueryParameters() != null ? context.getQueryParameters() : new QueryParametersImpl(); + Headers headers = + context.getHeaders() != null ? context.getHeaders() : Headers.emptyHeaders(); + + return new APIGatewayRequestEvent<>(queryParameters, body, context.getMethod(), context.getRequestURL(), headers); + } +} diff --git a/fn-events/src/main/java/com/fnproject/events/mapper/ApiGatewayRequestMapper.java b/fn-events/src/main/java/com/fnproject/events/mapper/ApiGatewayRequestMapper.java new file mode 100644 index 00000000..bb529d09 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/mapper/ApiGatewayRequestMapper.java @@ -0,0 +1,8 @@ +package com.fnproject.events.mapper; + +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; + +public interface ApiGatewayRequestMapper { + APIGatewayRequestEvent toApiGatewayRequestEvent(HTTPGatewayContext context, T body); +} diff --git a/fn-events/src/main/java/com/fnproject/events/output/APIGatewayResponseEvent.java b/fn-events/src/main/java/com/fnproject/events/output/APIGatewayResponseEvent.java new file mode 100644 index 00000000..f32a75f4 --- /dev/null +++ b/fn-events/src/main/java/com/fnproject/events/output/APIGatewayResponseEvent.java @@ -0,0 +1,52 @@ +package com.fnproject.events.output; + +import com.fnproject.fn.api.Headers; + +public class APIGatewayResponseEvent { + private final T body; + private final Integer statusCode; + private final Headers headers; + + private APIGatewayResponseEvent(T body, Integer statusCode, Headers headers) { + this.headers = headers; + this.body = body; + this.statusCode = statusCode; + } + + public static class Builder { + private T body; + private Integer statusCode; + private Headers headers; + + public Builder body(T body) { + this.body = body; + return this; + } + + public Builder statusCode(int statusCode) { + this.statusCode = statusCode; + return this; + } + + public Builder headers(Headers headers) { + this.headers = headers; + return this; + } + + public APIGatewayResponseEvent build() { + return new APIGatewayResponseEvent<>(this.body, this.statusCode, this.headers); + } + } + + public Integer getStatus() { + return statusCode; + } + + public Headers getHeaders() { + return headers; + } + + public T getBody() { + return body; + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/APIGatewayFunctionTest.java b/fn-events/src/test/java/com/fnproject/events/APIGatewayFunctionTest.java new file mode 100644 index 00000000..89f817f6 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/APIGatewayFunctionTest.java @@ -0,0 +1,93 @@ +package com.fnproject.events; + +import static org.junit.Assert.assertEquals; +import com.fnproject.events.testfns.apigatewayfns.APIGatewayTestFunction; +import com.fnproject.events.testfns.apigatewayfns.ListAPIGatewayTestFunction; +import com.fnproject.events.testfns.apigatewayfns.StringAPIGatewayTestFunction; +import com.fnproject.events.testfns.apigatewayfns.UncheckedAPIGatewayTestFunction; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class APIGatewayFunctionTest { + + @Rule + public final FnTestingRule fnRule = FnTestingRule.createDefault(); + + @Test + public void testStringHandler() { + fnRule + .givenEvent() + .withHeader("Fn-Http-H-Custom-Header", "headerValue") + .withHeader("Fn-Http-Method", "POST") + .withHeader("Fn-Http-Request-Url", "/v1?param1=value%20with%20spaces") + .withBody("plain string body") + .enqueue(); + + fnRule.thenRun(StringAPIGatewayTestFunction.class, "handler"); + + assertEquals("test response", fnRule.getOnlyResult().getBodyAsString()); + assertEquals("200", fnRule.getOnlyResult().getHeaders().get("Fn-Http-Status").get()); + assertEquals("headerValue", fnRule.getOnlyResult().getHeaders().get("Fn-Http-H-Custom-Header").get()); + } + + @Test + public void testObjectHandler() { + fnRule + .givenEvent() + .withHeader("Fn-Http-H-Custom-Header", "headerValue") + .withHeader("Fn-Http-Method", "POST") + .withHeader("Fn-Http-Request-Url", "/v1?param1=value%20with%20spaces") + .withBody("{\"name\":\"chicken\",\"age\":2}") + .enqueue(); + + fnRule.thenRun(APIGatewayTestFunction.class, "handler"); + + assertEquals("{\"brand\":\"ford\",\"wheels\":4}", fnRule.getOnlyResult().getBodyAsString()); + } + + @Test + public void testNullObjectPropertyHandler() { + fnRule + .givenEvent() + .withHeader("Fn-Http-H-Custom-Header", "headerValue") + .withHeader("Fn-Http-Method", "POST") + .withHeader("Fn-Http-Request-Url", "/v1?param1=value%20with%20spaces") + .withBody("{\"age\":2}") + .enqueue(); + + fnRule.thenRun(APIGatewayTestFunction.class, "handler"); + + assertEquals("{\"brand\":\"ford\",\"wheels\":4}", fnRule.getOnlyResult().getBodyAsString()); + } + + @Test + public void testListObjectHandler() { + fnRule + .givenEvent() + .withHeader("Fn-Http-H-Custom-Header", "headerValue") + .withHeader("Fn-Http-Method", "POST") + .withHeader("Fn-Http-Request-Url", "/v1?param1=value%20with%20spaces") + .withBody("[{\"name\":\"chicken\",\"age\":2}]") + .enqueue(); + + fnRule.thenRun(ListAPIGatewayTestFunction.class, "handler"); + + assertEquals("[{\"brand\":\"ford\",\"wheels\":4}]", fnRule.getOnlyResult().getBodyAsString()); + } + + @Test + public void testUncheckedHandler() { + fnRule + .givenEvent() + .withHeader("Fn-Http-H-Custom-Header", "headerValue") + .withHeader("Fn-Http-Method", "POST") + .withHeader("Fn-Http-Request-Url", "/v1?param1=value%20with%20spaces") + .withBody("plain string body") + .enqueue(); + + fnRule.thenRun(UncheckedAPIGatewayTestFunction.class, "handler"); + + assertEquals("test response", fnRule.getOnlyResult().getBodyAsString()); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/ConnectorHubFunctionTest.java b/fn-events/src/test/java/com/fnproject/events/ConnectorHubFunctionTest.java new file mode 100644 index 00000000..7e6d97c2 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/ConnectorHubFunctionTest.java @@ -0,0 +1,242 @@ +package com.fnproject.events; + +import static org.junit.Assert.assertEquals; +import java.util.Base64; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.testfns.Animal; +import com.fnproject.events.testfns.connectorhub.LoggingSourceTestFunction; +import com.fnproject.events.testfns.connectorhub.MonitorSourceTestFunction; +import com.fnproject.events.testfns.connectorhub.QueueSourceObjectTestFunction; +import com.fnproject.events.testfns.connectorhub.QueueSourceStringTestFunction; +import com.fnproject.events.testfns.connectorhub.StreamingSourceObjectTestFunction; +import com.fnproject.events.testfns.connectorhub.StreamingSourceStringTestFunction; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class ConnectorHubFunctionTest { + + @Rule + public final FnTestingRule fnRule = FnTestingRule.createDefault(); + + @Test + public void testMonitorSourceTestFunction() { + fnRule + .givenEvent() + .withBody("[\n" + + " {\n" + + " \"namespace\":\"oci_computeagent\",\n" + + " \"compartmentId\":\"ocid1.tenancy.oc1..exampleuniqueID\",\n" + + " \"name\":\"DiskBytesRead\",\n" + + " \"dimensions\":{\n" + + " \"resourceId\":\"ocid1.instance.region1.phx.exampleuniqueID\"\n" + + " },\n" + + " \"metadata\":{\n" + + " \"unit\":\"bytes\"\n" + + " },\n" + + " \"datapoints\":[\n" + + " {\n" + + " \"timestamp\":\"1761318377414\",\n" + + " \"value\":10.4\n" + + " },\n" + + " {\n" + + " \"timestamp\":\"1761318377414\",\n" + + " \"value\":11.3\n" + + " }\n" + + " ]\n" + + " }\n" + + "]") + .enqueue(); + + fnRule.thenRun(MonitorSourceTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testEmptyMonitorSourceTestFunction() { + fnRule + .givenEvent() + .withBody("[]") + .enqueue(); + + fnRule.thenRun(MonitorSourceTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testLoggingSourceTestFunction() { + fnRule + .givenEvent() + .withBody("[\n" + + " {\n" + + " \"data\": {\n" + + " \"applicationId\": \"ocid1.fnapp.oc1.abc\",\n" + + " \"containerId\": \"n/a\",\n" + + " \"functionId\": \"ocid1.fnfunc.oc1.abc\",\n" + + " \"message\": \"Received function invocation request\",\n" + + " \"opcRequestId\": \"/abc/def\",\n" + + " \"requestId\": \"/def/abc\",\n" + + " \"src\": \"stdout\"\n" + + " },\n" + + " \"id\": \"abc-zyx\",\n" + + " \"oracle\": {\n" + + " \"compartmentid\": \"ocid1.tenancy.oc1..xyz\",\n" + + " \"ingestedtime\": \"2025-10-23T15:45:19.457Z\",\n" + + " \"loggroupid\": \"ocid1.loggroup.oc1.abc\",\n" + + " \"logid\": \"ocid1.log.oc1.def\",\n" + + " \"tenantid\": \"ocid1.tenancy.oc1..xyz\"\n" + + " },\n" + + " \"source\": \"your-log\",\n" + + " \"specversion\": \"1.0\",\n" + + " \"subject\": \"schedule\",\n" + + " \"time\": \"2025-10-23T15:45:17.239Z\",\n" + + " \"type\": \"com.oraclecloud.functions.application.functioninvoke\"\n" + + " }\n" + + "]") + .enqueue(); + + fnRule.thenRun(LoggingSourceTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testEmptyLoggingSourceTestFunction() { + fnRule + .givenEvent() + .withBody("[]") + .enqueue(); + + fnRule.thenRun(LoggingSourceTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testStreamingSourceStringTestFunction() { + fnRule + .givenEvent() + .withBody("[" + + "{\"stream\":\"stream-name\"," + + "\"partition\":\"0\"," + + "\"key\":null," + + "\"value\":\"U2VudCBhIHBsYWluIG1lc3NhZ2U=\"," + + "\"offset\":3," + + "\"timestamp\":1761223385480" + + "}" + + "]") + .enqueue(); + + fnRule.thenRun(StreamingSourceStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testEmptyStreamingSourceStringTestFunction() { + fnRule + .givenEvent() + .withBody("[]") + .enqueue(); + + fnRule.thenRun(StreamingSourceStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testStreamingSourceObjectTestFunction() throws JsonProcessingException { + Animal animal = new Animal("foo", 4); + String encodedAnimal = Base64.getEncoder().encodeToString(new ObjectMapper().writeValueAsBytes(animal)); + fnRule + .givenEvent() + .withBody("[" + + "{\"stream\":\"stream-name\"," + + "\"partition\":\"0\"," + + "\"key\":null," + + "\"value\":\"" + encodedAnimal + "\"," + + "\"offset\":3," + + "\"timestamp\":1761223385480" + + "}" + + "]") + .enqueue(); + + fnRule.thenRun(StreamingSourceObjectTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testQueueSourceStringTestFunction() { + fnRule + .givenEvent() + .withBody("[\"a plain string, end\", \"another string\"]") + .enqueue(); + + fnRule.thenRun(QueueSourceStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testInvalidQueueSourceStringTestFunction() { + fnRule + .givenEvent() + .withBody("[a plain string, end, another string]") + .enqueue(); + + fnRule.thenRun(QueueSourceStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(2, exitCode); + } + + @Test + public void testQueueSourceEmptyTestFunction() { + fnRule + .givenEvent() + .withBody("[]") + .enqueue(); + + fnRule.thenRun(QueueSourceStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testQueueSourceObjectTestFunction() { + fnRule + .givenEvent() + .withBody("[{\"name\":\"foo\",\"age\":3}]") + .enqueue(); + + fnRule.thenRun(QueueSourceObjectTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testSourceWithoutBodyThrows() { + fnRule + .givenEvent() + .enqueue(); + + fnRule.thenRun(QueueSourceStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(2, exitCode); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/NotificationFunctionTest.java b/fn-events/src/test/java/com/fnproject/events/NotificationFunctionTest.java new file mode 100644 index 00000000..6628c2c2 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/NotificationFunctionTest.java @@ -0,0 +1,65 @@ +package com.fnproject.events; + +import static org.junit.Assert.assertEquals; +import com.fnproject.events.testfns.notification.NotificationObjectTestFunction; +import com.fnproject.events.testfns.notification.NotificationStringTestFunction; +import com.fnproject.fn.testing.FnTestingRule; +import org.junit.Rule; +import org.junit.Test; + +public class NotificationFunctionTest { + + @Rule + public final FnTestingRule fnRule = FnTestingRule.createDefault(); + + @Test + public void testNotificationTestFunction() { + fnRule + .givenEvent() + .withBody("{\"name\":\"foo\",\"age\":3}") + .enqueue(); + + fnRule.thenRun(NotificationObjectTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testNotificationStringTestFunction() { + fnRule + .givenEvent() + .withBody("test string") + .enqueue(); + + fnRule.thenRun(NotificationStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testBlankNotificationStringTestFunction() { + fnRule + .givenEvent() + .withBody("") + .enqueue(); + + fnRule.thenRun(NotificationStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } + + @Test + public void testBlankNotificationWithoutBodyTestFunction() { + fnRule + .givenEvent() + .enqueue(); + + fnRule.thenRun(NotificationStringTestFunction.class, "handler"); + + int exitCode = fnRule.getLastExitCode(); + assertEquals(0, exitCode); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/coercion/APIGatewayCoercionTest.java b/fn-events/src/test/java/com/fnproject/events/coercion/APIGatewayCoercionTest.java new file mode 100644 index 00000000..0d92df1a --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/coercion/APIGatewayCoercionTest.java @@ -0,0 +1,398 @@ +package com.fnproject.events.coercion; + +import static com.fnproject.events.coercion.APIGatewayCoercion.OM_KEY; +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.events.testfns.Car; +import com.fnproject.events.testfns.apigatewayfns.APIGatewayTestFunction; +import com.fnproject.events.testfns.Animal; +import com.fnproject.events.testfns.apigatewayfns.GrandChildGatewayTestFunction; +import com.fnproject.events.testfns.apigatewayfns.ListAPIGatewayTestFunction; +import com.fnproject.events.testfns.apigatewayfns.StringAPIGatewayTestFunction; +import com.fnproject.events.testfns.apigatewayfns.UncheckedAPIGatewayTestFunction; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.runtime.DefaultFunctionInvocationContext; +import com.fnproject.fn.runtime.DefaultMethodWrapper; +import com.fnproject.fn.runtime.FunctionRuntimeContext; +import com.fnproject.fn.runtime.ReadOnceInputEvent; +import org.junit.Before; +import org.junit.Test; + + +public class APIGatewayCoercionTest { + private APIGatewayCoercion coercion; + private InvocationContext requestinvocationContext; + private DefaultFunctionInvocationContext responseInvocationContext; + + @Before + public void setUp() { + coercion = APIGatewayCoercion.instance(); + requestinvocationContext = mock(InvocationContext.class); + RuntimeContext runtimeContext = mock(RuntimeContext.class); + ObjectMapper mapper = new ObjectMapper(); + + when(runtimeContext.getAttribute(OM_KEY, ObjectMapper.class)).thenReturn(Optional.of(mapper)); + when(requestinvocationContext.getRuntimeContext()).thenReturn(runtimeContext); + } + + @Test + public void testReturnEmptyWhenNotAPIGatewayClass() { + MethodWrapper method = new DefaultMethodWrapper(APIGatewayCoercionTest.class, "testMethod"); + + Headers headers = Headers.emptyHeaders(); + + when(requestinvocationContext.getRequestHeaders()).thenReturn(headers); + ByteArrayInputStream is = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); + InputEvent inputEvent = new ReadOnceInputEvent(is, Headers.emptyHeaders(), "call", Instant.now()); + Optional requestEvent = coercion.tryCoerceParam(requestinvocationContext, 0, inputEvent, method); + + assertFalse(requestEvent.isPresent()); + } + + @Test + public void testRequestHeaders() { + MethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + + Headers h = Headers.emptyHeaders() + .setHeader("H1", "h1val") + .setHeader("Fn-Http-H-", "ignored") + .setHeader("Fn-Http-H-A", "b") + .setHeader("Fn-Http-H-mv", "c", "d"); + + APIGatewayRequestEvent requestEvent = coerceRequest(method, h, ""); + + assertEquals("b", (requestEvent.getHeaders().get("A")).get()); + assertEquals("c", (requestEvent.getHeaders().getAllValues("Mv")).get(0)); + assertEquals("d", (requestEvent.getHeaders().getAllValues("Mv")).get(1)); + assertFalse(requestEvent.getHeaders().get("H1").isPresent()); + } + + @Test + public void testRequestUrl() { + MethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + + Headers h = Headers.emptyHeaders() + .setHeader("Fn-Http-Request-Url", "/v2/employee/123?param1=value%20with%20spaces"); + + APIGatewayRequestEvent requestEvent = coerceRequest(method, h, ""); + + assertEquals("/v2/employee/123?param1=value%20with%20spaces", requestEvent.getRequestUrl()); + } + + @Test + public void testRequestMethod() { + MethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + + Headers h = Headers.emptyHeaders() + .setHeader("Fn-Http-Method", "PATCH"); + + APIGatewayRequestEvent requestEvent = coerceRequest(method, h, ""); + + assertEquals("PATCH", requestEvent.getMethod()); + } + + @Test + public void testQueryParameters() { + MethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + + Headers h = Headers.emptyHeaders() + .setHeader("Fn-Http-Request-Url", "/v2/employee/123?param1=value%20with%20spaces&repeat=2&repeat=3"); + + APIGatewayRequestEvent requestEvent = coerceRequest(method, h, ""); + + assertEquals("value with spaces", requestEvent.getQueryParameters().get("param1").get()); + } + + @Test + public void testQueryRepeatedParameters() { + MethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + + Headers h = Headers.emptyHeaders() + .setHeader("Fn-Http-Request-Url", "/v2/employee/123?param1=value%20with%20spaces&repeat=2&repeat=3"); + + APIGatewayRequestEvent requestEvent = coerceRequest(method, h, ""); + + assertEquals("2", (requestEvent.getQueryParameters().getValues("repeat")).get(0)); + assertEquals("3", (requestEvent.getQueryParameters().getValues("repeat")).get(1)); + } + + @Test + public void testRequestStringBody() { + MethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + APIGatewayRequestEvent requestEvent = coerceRequest(method, "simple string"); + + assertEquals("simple string", requestEvent.getBody()); + } + + @Test + public void testListObjectRequestBody() { + MethodWrapper method = new DefaultMethodWrapper(ListAPIGatewayTestFunction.class, "handler"); + APIGatewayRequestEvent requestEvent = coerceRequest(method, "[{\"name\":\"Spot\",\"age\":6}]"); + + assertEquals("Spot", ((List) requestEvent.getBody()).get(0).getName()); + assertEquals(6, ((List) requestEvent.getBody()).get(0).getAge()); + } + + @Test + public void testSingleObjectRequestBody() { + MethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + APIGatewayRequestEvent requestEvent = coerceRequest(method, "{\"name\":\"Spot\",\"age\":6}"); + + assertEquals("Spot", ((Animal) requestEvent.getBody()).getName()); + assertEquals(6, ((Animal) requestEvent.getBody()).getAge()); + } + + @Test + public void testUncheckedHandler() { + MethodWrapper method = new DefaultMethodWrapper(UncheckedAPIGatewayTestFunction.class, "handler"); + Headers h = Headers.emptyHeaders().setHeader("H1", "h1val"); + + APIGatewayRequestEvent requestEvent = coerceRequest(method, h, "{\"name\":\"Spot\",\"age\":6}"); + + assertEquals("{\"name\":\"Spot\",\"age\":6}", requestEvent.getBody()); + } + + @Test + public void testFailureToParseIsUserFriendlyError() { + MethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + RuntimeException exception = assertThrows(RuntimeException.class, () -> coerceRequest(method, "INVALID JSON")); + + assertEquals("Failed to coerce event to user function parameter type [simple type, class com.fnproject.events.testfns.Animal]", exception.getMessage()); + assertTrue(exception.getCause().getMessage().startsWith("Unrecognized token 'INVALID':")); + } + + @Test + public void testCoerceForGrandChild() { + MethodWrapper method = new DefaultMethodWrapper(GrandChildGatewayTestFunction.class, "handler"); + APIGatewayRequestEvent requestEvent = coerceRequest(method, "simple string"); + + assertEquals("simple string", requestEvent.getBody()); + } + + @Test + public void testReturnEmptyResponseWhenNotAPIGatewayClass() { + MethodWrapper method = new DefaultMethodWrapper(APIGatewayCoercionTest.class, "testMethod"); + + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder().build(); + Optional outputEvent = coercion.wrapFunctionResult(responseInvocationContext, method, responseEvent); + + assertFalse(outputEvent.isPresent()); + } + + @Test + public void testResponseHeaders() { + Headers headers = Headers.emptyHeaders() + .addHeader("custom-header", "customValue") + .setHeaders(Collections.singletonMap("custom-header-2", Collections.singletonList("customValue2"))); + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .headers(headers) + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("customValue", + responseInvocationContext.getAdditionalResponseHeaders().get("Fn-Http-H-Custom-Header").get(0)); + assertEquals("customValue2", + responseInvocationContext.getAdditionalResponseHeaders().get("Fn-Http-H-Custom-Header-2").get(0)); + } + + @Test + public void testResponseRepeatHeaders() { + Headers headers = Headers.emptyHeaders() + .addHeader("repeat", "1") + .addHeader("repeat", "2"); + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .headers(headers) + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("2", + responseInvocationContext.getAdditionalResponseHeaders().get("Fn-Http-H-Repeat").get(1)); + assertEquals("1", + responseInvocationContext.getAdditionalResponseHeaders().get("Fn-Http-H-Repeat").get(0)); + } + + @Test + public void testResponseContentTypeDefaultString() { + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("text/plain", outputEvent.get().getContentType().get()); + } + + @Test + public void testResponseContentTypeDefault() { + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("application/json", outputEvent.get().getContentType().get()); + } + + @Test + public void testResponseCustomContentType() { + Headers headers = Headers.emptyHeaders() + .addHeader("content-type", "application/octet-stream"); + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .headers(headers) + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("application/octet-stream", outputEvent.get().getContentType().get()); + } + + @Test + public void testResponseStatus() { + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .statusCode(200) + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("200", responseInvocationContext.getAdditionalResponseHeaders().get("Fn-Http-Status").get(0)); + } + + @Test + public void testResponseStringBody() throws IOException { + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .body("string body") + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + String actual = writeToString(outputEvent.get()); + assertEquals("string body", actual); + } + + @Test + public void testResponseObjectBody() throws IOException { + Car car = new Car("ford", 4); + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .body(car) + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + String actual = writeToString(outputEvent.get()); + assertEquals("{\"brand\":\"ford\",\"wheels\":4}", actual); + } + + @Test + public void testResponseListBody() throws IOException { + Car car = new Car("ford", 4); + APIGatewayResponseEvent> responseEvent = new APIGatewayResponseEvent.Builder>() + .body(Collections.singletonList(car)) + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(APIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + String actual = writeToString(outputEvent.get()); + assertEquals("[{\"brand\":\"ford\",\"wheels\":4}]", actual); + } + + @Test + public void testResponseStringNullBody() { + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(StringAPIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("text/plain", outputEvent.get().getContentType().get()); + } + + @Test + public void testResponseUncheckedBody() { + APIGatewayResponseEvent responseEvent = new APIGatewayResponseEvent.Builder() + .build(); + + DefaultMethodWrapper method = new DefaultMethodWrapper(UncheckedAPIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(responseEvent, method); + + assertTrue(outputEvent.isPresent()); + assertEquals("text/plain", outputEvent.get().getContentType().get()); + } + + @Test + public void testNullResponse() { + DefaultMethodWrapper method = new DefaultMethodWrapper(UncheckedAPIGatewayTestFunction.class, "handler"); + Optional outputEvent = wrap(null, method); + + assertFalse(outputEvent.isPresent()); + } + + private Optional wrap(APIGatewayResponseEvent responseEvent, MethodWrapper method) { + Headers requestHeaders = Headers.emptyHeaders(); + InputEvent inputEvent = mock(InputEvent.class); + when(inputEvent.getHeaders()).thenReturn(requestHeaders); + + FunctionRuntimeContext frc = new FunctionRuntimeContext(method, new HashMap<>()); + + responseInvocationContext = new DefaultFunctionInvocationContext(frc, inputEvent); + return coercion.wrapFunctionResult(responseInvocationContext, method, responseEvent); + } + + private static String writeToString(OutputEvent oe) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + oe.writeToOutput(baos); + return baos.toString("UTF-8"); + } + + private APIGatewayRequestEvent coerceRequest(MethodWrapper method, Headers headers, String body) { + when(requestinvocationContext.getRequestHeaders()).thenReturn(headers); + ByteArrayInputStream is = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); + InputEvent inputEvent = new ReadOnceInputEvent(is, Headers.emptyHeaders(), "call", Instant.now()); + return coercion.tryCoerceParam(requestinvocationContext, 0, inputEvent, method).orElseThrow(RuntimeException::new); + } + + private APIGatewayRequestEvent coerceRequest(MethodWrapper method, String body) { + return coerceRequest(method, Headers.emptyHeaders(), body); + } + + public String testMethod(List ss) { + // This method isn't actually called, it only exists to have its parameter types examined by the JacksonCoercion + return ss.get(0).getName(); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/coercion/ConnectorHubCoercionTest.java b/fn-events/src/test/java/com/fnproject/events/coercion/ConnectorHubCoercionTest.java new file mode 100644 index 00000000..19b61a74 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/coercion/ConnectorHubCoercionTest.java @@ -0,0 +1,398 @@ +package com.fnproject.events.coercion; + +import static com.fnproject.events.coercion.APIGatewayCoercion.OM_KEY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.Date; +import java.util.Optional; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.LoggingData; +import com.fnproject.events.input.sch.MetricData; +import com.fnproject.events.input.sch.StreamingData; +import com.fnproject.events.testfns.Animal; +import com.fnproject.events.testfns.connectorhub.GrandChildMonitorSourceTestFunction; +import com.fnproject.events.testfns.connectorhub.LoggingSourceTestFunction; +import com.fnproject.events.testfns.connectorhub.MonitorSourceTestFunction; +import com.fnproject.events.testfns.connectorhub.QueueSourceObjectTestFunction; +import com.fnproject.events.testfns.connectorhub.QueueSourceStringTestFunction; +import com.fnproject.events.testfns.connectorhub.StreamingSourceObjectTestFunction; +import com.fnproject.events.testfns.connectorhub.StreamingSourceStringTestFunction; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.runtime.DefaultMethodWrapper; +import com.fnproject.fn.runtime.ReadOnceInputEvent; +import org.junit.Before; +import org.junit.Test; + +public class ConnectorHubCoercionTest { + private ConnectorHubCoercion coercion; + private InvocationContext requestinvocationContext; + + @Before + public void setUp() { + coercion = ConnectorHubCoercion.instance(); + requestinvocationContext = mock(InvocationContext.class); + RuntimeContext runtimeContext = mock(RuntimeContext.class); + ObjectMapper mapper = new ObjectMapper(); + + when(runtimeContext.getAttribute(OM_KEY, ObjectMapper.class)).thenReturn(Optional.of(mapper)); + when(requestinvocationContext.getRuntimeContext()).thenReturn(runtimeContext); + } + + @Test + public void testReturnEmptyWhenNotConnectorHubEventClass() { + MethodWrapper method = new DefaultMethodWrapper(APIGatewayCoercionTest.class, "testMethod"); + + Headers headers = Headers.emptyHeaders(); + + when(requestinvocationContext.getRequestHeaders()).thenReturn(headers); + ByteArrayInputStream is = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); + InputEvent inputEvent = new ReadOnceInputEvent(is, Headers.emptyHeaders(), "call", Instant.now()); + Optional> batch = coercion.tryCoerceParam(requestinvocationContext, 0, inputEvent, method); + + assertFalse(batch.isPresent()); + } + + @Test + public void testMonitoringSourceInput() { + MethodWrapper method = new DefaultMethodWrapper(MonitorSourceTestFunction.class, "handler"); + + ConnectorHubBatch event = coerceRequest(method, "[\n" + + " {\n" + + " \"namespace\": \"oci_objectstorage\",\n" + + " \"resourceGroup\": \"nullable\",\n" + + " \"compartmentId\": \"ocid1.tenancy.oc1..xyz\",\n" + + " \"name\": \"PutRequests\",\n" + + " \"dimensions\": {\n" + + " \"resourceID\": \"ocid1.bucket.oc1.uk-london-1.xyz\",\n" + + " \"resourceDisplayName\": \"foo\"\n" + + " },\n" + + " \"metadata\": {\n" + + " \"displayName\": \"PutObject Request Count\",\n" + + " \"unit\": \"count\"\n" + + " },\n" + + " \"datapoints\": [\n" + + " {\n" + + " \"timestamp\": 1761318377414,\n" + + " \"value\": 1.0,\n" + + " \"count\": 1\n" + + " }\n" + + " ]\n" + + " }," + + " {\n" + + " \"namespace\": \"oci_objectstorage\",\n" + + " \"resourceGroup\": null,\n" + + " \"compartmentId\": \"ocid1.tenancy.oc1..abc\",\n" + + " \"name\": \"PutRequests\",\n" + + " \"dimensions\": {\n" + + " \"resourceID\": \"ocid1.bucket.oc1.uk-london-1.abc\",\n" + + " \"resourceDisplayName\": \"bar\"\n" + + " },\n" + + " \"metadata\": {\n" + + " \"displayName\": \"PutObject Request Count\",\n" + + " \"unit\": \"count\"\n" + + " },\n" + + " \"datapoints\": [\n" + + " {\n" + + " \"timestamp\": 1761318377414,\n" + + " \"value\": 1.0,\n" + + " \"count\": 1\n" + + " },\n" + + " {\n" + + " \"timestamp\": 1761318377614,\n" + + " \"value\": 2.0,\n" + + " \"count\": 1\n" + + " }\n" + + " ]\n" + + " }" + + "]"); + + assertFalse(event.getBatch().isEmpty()); + assertEquals(2, event.getBatch().size()); + MetricData monitoringSource = event.getBatch().get(0); + assertEquals("PutRequests", monitoringSource.getName()); + assertEquals("nullable", monitoringSource.getResourceGroup()); + assertEquals("ocid1.tenancy.oc1..xyz", monitoringSource.getCompartmentId()); + assertEquals("oci_objectstorage", monitoringSource.getNamespace()); + assertEquals("ocid1.bucket.oc1.uk-london-1.xyz", monitoringSource.getDimensions().get("resourceID")); + assertEquals("foo", monitoringSource.getDimensions().get("resourceDisplayName")); + assertEquals("PutObject Request Count", monitoringSource.getMetadata().get("displayName")); + assertEquals("count", monitoringSource.getMetadata().get("unit")); + assertEquals(Integer.valueOf(1), monitoringSource.getDatapoints().get(0).getCount()); + assertEquals(Double.parseDouble("1.0"), monitoringSource.getDatapoints().get(0).getValue(), 0); + assertEquals(Date.from(Instant.ofEpochMilli(Long.parseLong("1761318377414"))), monitoringSource.getDatapoints().get(0).getTimestamp()); + } + + @Test + public void testMonitoringSourceInputNoCount() { + MethodWrapper method = new DefaultMethodWrapper(MonitorSourceTestFunction.class, "handler"); + + ConnectorHubBatch event = coerceRequest(method, "[\n" + + " {\n" + + " \"namespace\":\"oci_computeagent\",\n" + + " \"compartmentId\":\"ocid1.tenancy.oc1..exampleuniqueID\",\n" + + " \"name\":\"DiskBytesRead\",\n" + + " \"dimensions\":{\n" + + " \"resourceId\":\"ocid1.instance.region1.phx.exampleuniqueID\"\n" + + " },\n" + + " \"metadata\":{\n" + + " \"unit\":\"bytes\"\n" + + " },\n" + + " \"datapoints\":[\n" + + " {\n" + + " \"timestamp\":\"1761318377414\",\n" + + " \"value\":10.4\n" + + " },\n" + + " {\n" + + " \"timestamp\":\"1761318377414\",\n" + + " \"value\":11.3\n" + + " }\n" + + " ]\n" + + " }\n" + + "]"); + + assertFalse(event.getBatch().isEmpty()); + assertEquals(1, event.getBatch().size()); + MetricData monitoringSource = event.getBatch().get(0); + assertEquals("DiskBytesRead", monitoringSource.getName()); + assertEquals("ocid1.tenancy.oc1..exampleuniqueID", monitoringSource.getCompartmentId()); + assertEquals("oci_computeagent", monitoringSource.getNamespace()); + assertNull(monitoringSource.getDimensions().get("resourceID")); + assertNull(monitoringSource.getDimensions().get("resourceDisplayName")); + assertNull(monitoringSource.getMetadata().get("displayName")); + assertEquals("bytes", monitoringSource.getMetadata().get("unit")); + assertEquals(Double.parseDouble("10.4"), monitoringSource.getDatapoints().get(0).getValue(), 0); + assertEquals(Date.from(Instant.ofEpochMilli(Long.parseLong("1761318377414"))), monitoringSource.getDatapoints().get(0).getTimestamp()); + } + + @Test + public void testMonitoringSourceInputEmpty() { + MethodWrapper method = new DefaultMethodWrapper(MonitorSourceTestFunction.class, "handler"); + + ConnectorHubBatch event = coerceRequest(method, "[]"); + + assertTrue(event.getBatch().isEmpty()); + } + + @Test + public void testGrandChildIsCoercedInputEmpty() { + MethodWrapper method = new DefaultMethodWrapper(GrandChildMonitorSourceTestFunction.class, "handler"); + + ConnectorHubBatch event = coerceRequest(method, "[]"); + + assertTrue(event.getBatch().isEmpty()); + } + + @Test + public void testFailureToParseIsUserFriendlyError() { + MethodWrapper method = new DefaultMethodWrapper(MonitorSourceTestFunction.class, "handler"); + RuntimeException exception = assertThrows(RuntimeException.class, () -> coerceRequest(method, "INVALID JSON")); + + assertEquals( + "Failed to coerce event to user function parameter type [collection type; class java.util.List, contains [simple type, class com.fnproject.events.input.sch.MetricData]]", + exception.getMessage()); + assertTrue(exception.getCause().getMessage().startsWith("Unrecognized token 'INVALID':")); + } + + @Test + public void testLoggingSourceInput() { + MethodWrapper method = new DefaultMethodWrapper(LoggingSourceTestFunction.class, "handler"); + + ConnectorHubBatch event = coerceRequest(method, "[\n" + + " {\n" + + " \"data\": {\n" + + " \"applicationId\": \"ocid1.fnapp.oc1.abc\",\n" + + " \"containerId\": \"n/a\",\n" + + " \"functionId\": \"ocid1.fnfunc.oc1.abc\",\n" + + " \"message\": \"Received function invocation request\",\n" + + " \"opcRequestId\": \"/abc/def\",\n" + + " \"requestId\": \"/def/abc\",\n" + + " \"src\": \"stdout\"\n" + + " },\n" + + " \"id\": \"abc-zyx\",\n" + + " \"oracle\": {\n" + + " \"compartmentid\": \"ocid1.tenancy.oc1..xyz\",\n" + + " \"ingestedtime\": \"2025-10-23T15:45:19.457Z\",\n" + + " \"loggroupid\": \"ocid1.loggroup.oc1.abc\",\n" + + " \"logid\": \"ocid1.log.oc1.def\",\n" + + " \"tenantid\": \"ocid1.tenancy.oc1..xyz\"\n" + + " },\n" + + " \"source\": \"your-log\",\n" + + " \"specversion\": \"1.0\",\n" + + " \"subject\": \"schedule\",\n" + + " \"time\": \"2025-10-24T15:06:17.000Z\",\n" + + " \"type\": \"com.oraclecloud.functions.application.functioninvoke\"\n" + + " },\n" + + " {\n" + + " \"data\": {\n" + + " \"applicationId\": \"ocid1.fnapp.oc1.def\",\n" + + " \"containerId\": \"n/a\",\n" + + " \"functionId\": \"ocid1.fnfunc.oc1.def\",\n" + + " \"message\": \"Received function invocation request\",\n" + + " \"opcRequestId\": \"/def/xyz\",\n" + + " \"requestId\": \"/foo/bar\",\n" + + " \"src\": \"stdout\"\n" + + " },\n" + + " \"id\": \"foo-zyx\",\n" + + " \"oracle\": {\n" + + " \"compartmentid\": \"ocid1.tenancy.oc1..xyz\",\n" + + " \"ingestedtime\": \"2025-11-23T15:45:19.457Z\",\n" + + " \"loggroupid\": \"ocid1.loggroup.oc1.def\",\n" + + " \"logid\": \"ocid1.log.oc1.xyz\",\n" + + " \"tenantid\": \"ocid1.tenancy.oc1..xyz\"\n" + + " },\n" + + " \"source\": \"your-log\",\n" + + " \"specversion\": \"1.0\",\n" + + " \"subject\": \"schedule\",\n" + + " \"time\": \"2025-11-23T15:45:17.239Z\",\n" + + " \"type\": \"com.oraclecloud.functions.application.functioninvoke\"\n" + + " }\n" + + "]"); + + assertFalse(event.getBatch().isEmpty()); + assertEquals(2, event.getBatch().size()); + LoggingData loggingData = event.getBatch().get(0); + assertEquals("your-log", loggingData.getSource()); + assertEquals("abc-zyx", loggingData.getId()); + assertEquals("schedule", loggingData.getSubject()); + assertEquals("1.0", loggingData.getSpecversion()); + assertEquals("ocid1.fnapp.oc1.abc", loggingData.getData().get("applicationId")); + assertEquals("n/a", loggingData.getData().get("containerId")); + assertEquals("ocid1.fnfunc.oc1.abc", loggingData.getData().get("functionId")); + assertEquals("Received function invocation request", loggingData.getData().get("message")); + assertEquals("/abc/def", loggingData.getData().get("opcRequestId")); + assertEquals("/def/abc", loggingData.getData().get("requestId")); + assertEquals("stdout", loggingData.getData().get("src")); + assertEquals("ocid1.tenancy.oc1..xyz", loggingData.getOracle().get("compartmentid")); + assertEquals("2025-10-23T15:45:19.457Z", loggingData.getOracle().get("ingestedtime")); + assertEquals("ocid1.loggroup.oc1.abc", loggingData.getOracle().get("loggroupid")); + assertEquals("ocid1.log.oc1.def", loggingData.getOracle().get("logid")); + assertEquals("ocid1.tenancy.oc1..xyz", loggingData.getOracle().get("tenantid")); + assertEquals(Date.from(Instant.ofEpochMilli(Long.parseLong("1761318377000"))), loggingData.getTime()); + assertEquals("com.oraclecloud.functions.application.functioninvoke", loggingData.getType()); + } + + @Test + public void testStreamingSourceInput() { + MethodWrapper method = new DefaultMethodWrapper(StreamingSourceStringTestFunction.class, "handler"); + + ConnectorHubBatch> event = coerceRequest(method, "[" + + "{\"stream\":\"stream-name\"," + + "\"partition\":\"0\"," + + "\"key\":null," + + "\"value\":\"U2VudCBhIHBsYWluIG1lc3NhZ2U=\"," + + "\"offset\":3," + + "\"timestamp\":1761223385480" + + "}," + + "{\"stream\":\"stream-name\"," + + "\"partition\":\"0\"," + + "\"key\":null," + + "\"value\":\"U2VudCBhIHBsYWluIG1lc3NhZ2U=\"," + + "\"offset\":3," + + "\"timestamp\":1761223385480" + + "}" + + "]"); + + assertFalse(event.getBatch().isEmpty()); + assertEquals(2, event.getBatch().size()); + StreamingData streamingData = event.getBatch().get(0); + assertEquals("stream-name", streamingData.getStream()); + assertNull(streamingData.getKey()); + assertEquals("3", streamingData.getOffset()); + assertEquals("0", streamingData.getPartition()); + assertEquals(Date.from(Instant.ofEpochMilli(Long.parseLong("1761223385480"))), streamingData.getTimestamp()); + assertEquals("Sent a plain message", streamingData.getValue()); + } + + @Test + public void testStreamingSourceInputObject() throws JsonProcessingException { + MethodWrapper method = new DefaultMethodWrapper(StreamingSourceObjectTestFunction.class, "handler"); + Animal animal = new Animal("cat", 2); + + ConnectorHubBatch> event = coerceRequest(method, "[" + + "{\"stream\":\"stream-name\"," + + "\"partition\":\"0\"," + + "\"key\":null," + + "\"value\":\"" + Base64.getEncoder().encodeToString(new ObjectMapper().writeValueAsBytes(animal)) + "\"," + + "\"offset\":3," + + "\"timestamp\":1761223385480" + + "}" + + "]"); + + assertFalse(event.getBatch().isEmpty()); + assertEquals(1, event.getBatch().size()); + StreamingData streamingData = event.getBatch().get(0); + assertEquals("stream-name", streamingData.getStream()); + assertNull(streamingData.getKey()); + assertEquals("3", streamingData.getOffset()); + assertEquals("0", streamingData.getPartition()); + assertEquals(animal.getAge(), streamingData.getValue().getAge()); + assertEquals(Date.from(Instant.ofEpochMilli(Long.parseLong("1761223385480"))), streamingData.getTimestamp()); + } + + @Test + public void testQueueSourceInputInvalidMessage() { + MethodWrapper method = new DefaultMethodWrapper(QueueSourceStringTestFunction.class, "handler"); + assertThrows(RuntimeException.class, () -> coerceRequest(method, "[a plain string]")); + } + + @Test + public void testQueueSourceInputString() { + MethodWrapper method = new DefaultMethodWrapper(QueueSourceStringTestFunction.class, "handler"); + ConnectorHubBatch event = coerceRequest(method, "[\"a plain , comma string 1\",\"a plain , comma string 2\",\"a plain , comma string 3\"]"); + + assertFalse(event.getBatch().isEmpty()); + assertEquals(3, event.getBatch().size()); + assertEquals("a plain , comma string 1", event.getBatch().get(0)); + } + + @Test + public void testQueueSourceInputObject() { + Animal animal = new Animal("cat", 2); + MethodWrapper method = new DefaultMethodWrapper(QueueSourceObjectTestFunction.class, "handler"); + ConnectorHubBatch event = coerceRequest(method, "[{\"name\": \"cat\",\"age\":2}]"); + + assertFalse(event.getBatch().isEmpty()); + assertEquals(1, event.getBatch().size()); + assertEquals(animal, event.getBatch().get(0)); + } + + @Test + public void testHeaders() { + MethodWrapper method = new DefaultMethodWrapper(QueueSourceObjectTestFunction.class, "handler"); + when(requestinvocationContext.getRequestHeaders()).thenReturn(Headers.emptyHeaders().addHeader("foo", "bar")); + ConnectorHubBatch event = coerceRequest(method, "[{\"name\": \"cat\",\"age\":2}]"); + + assertEquals("bar", event.getHeaders().get("foo").get()); + } + + @Test + public void testEmptyHeaders() { + MethodWrapper method = new DefaultMethodWrapper(QueueSourceObjectTestFunction.class, "handler"); + when(requestinvocationContext.getRequestHeaders()).thenReturn(Headers.emptyHeaders()); + ConnectorHubBatch event = coerceRequest(method, "[{\"name\": \"cat\",\"age\":2}]"); + + assertEquals(0, event.getHeaders().asMap().size()); + } + + private ConnectorHubBatch coerceRequest(MethodWrapper method, String body) { + ByteArrayInputStream is = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); + InputEvent inputEvent = new ReadOnceInputEvent(is, Headers.emptyHeaders(), "call", Instant.now()); + return coercion.tryCoerceParam(requestinvocationContext, 0, inputEvent, method).orElseThrow(RuntimeException::new); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/coercion/NotificationCoercionTest.java b/fn-events/src/test/java/com/fnproject/events/coercion/NotificationCoercionTest.java new file mode 100644 index 00000000..9b362e9b --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/coercion/NotificationCoercionTest.java @@ -0,0 +1,107 @@ +package com.fnproject.events.coercion; + +import static com.fnproject.events.coercion.APIGatewayCoercion.OM_KEY; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.util.Optional; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.events.testfns.Animal; +import com.fnproject.events.testfns.notification.NotificationObjectTestFunction; +import com.fnproject.events.testfns.notification.NotificationStringTestFunction; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.runtime.DefaultMethodWrapper; +import com.fnproject.fn.runtime.ReadOnceInputEvent; +import org.junit.Before; +import org.junit.Test; + + +public class NotificationCoercionTest { + private NotificationCoercion coercion; + private InvocationContext requestinvocationContext; + + @Before + public void setUp() { + coercion = NotificationCoercion.instance(); + requestinvocationContext = mock(InvocationContext.class); + RuntimeContext runtimeContext = mock(RuntimeContext.class); + ObjectMapper mapper = new ObjectMapper(); + + when(runtimeContext.getAttribute(OM_KEY, ObjectMapper.class)).thenReturn(Optional.of(mapper)); + when(requestinvocationContext.getRuntimeContext()).thenReturn(runtimeContext); + } + + @Test + public void testReturnEmptyWhenNotNotificationClass() { + MethodWrapper method = new DefaultMethodWrapper(APIGatewayCoercionTest.class, "testMethod"); + + Headers headers = Headers.emptyHeaders(); + + when(requestinvocationContext.getRequestHeaders()).thenReturn(headers); + ByteArrayInputStream is = new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8)); + InputEvent inputEvent = new ReadOnceInputEvent(is, Headers.emptyHeaders(), "call", Instant.now()); + Optional batch = coercion.tryCoerceParam(requestinvocationContext, 0, inputEvent, method); + + assertFalse(batch.isPresent()); + } + + @Test + public void testNotificationObjectInput() { + MethodWrapper method = new DefaultMethodWrapper(NotificationObjectTestFunction.class, "handler"); + + NotificationMessage event = coerceRequest(method, "{\"name\":\"foo\",\"age\":3}"); + + assertEquals(3, event.getContent().getAge()); + assertEquals("foo", event.getContent().getName()); + } + + @Test + public void testNotificationStringInput() { + MethodWrapper method = new DefaultMethodWrapper(NotificationStringTestFunction.class, "handler"); + NotificationMessage event = coerceRequest(method, "a plain string"); + + assertEquals("a plain string", event.getContent()); + } + + @Test + public void testNotificationStringInputEmpty() { + MethodWrapper method = new DefaultMethodWrapper(NotificationStringTestFunction.class, "handler"); + + NotificationMessage event = coerceRequest(method, ""); + + assertEquals("", event.getContent()); + } + + @Test + public void testHeaders() { + MethodWrapper method = new DefaultMethodWrapper(NotificationObjectTestFunction.class, "handler"); + when(requestinvocationContext.getRequestHeaders()).thenReturn(Headers.emptyHeaders().addHeader("foo", "bar")); + NotificationMessage event = coerceRequest(method, "{\"name\": \"cat\",\"age\":2}"); + + assertEquals("bar", event.getHeaders().get("foo").get()); + } + + @Test + public void testEmptyHeaders() { + MethodWrapper method = new DefaultMethodWrapper(NotificationObjectTestFunction.class, "handler"); + when(requestinvocationContext.getRequestHeaders()).thenReturn(Headers.emptyHeaders()); + NotificationMessage event = coerceRequest(method, "{\"name\": \"cat\",\"age\":2}"); + + assertEquals(0, event.getHeaders().asMap().size()); + } + + private NotificationMessage coerceRequest(MethodWrapper method, String body) { + ByteArrayInputStream is = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)); + InputEvent inputEvent = new ReadOnceInputEvent(is, Headers.emptyHeaders(), "call", Instant.now()); + return coercion.tryCoerceParam(requestinvocationContext, 0, inputEvent, method).orElseThrow(RuntimeException::new); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/coercion/jackson/Base64ToTypeDeserializerTest.java b/fn-events/src/test/java/com/fnproject/events/coercion/jackson/Base64ToTypeDeserializerTest.java new file mode 100644 index 00000000..269b89fd --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/coercion/jackson/Base64ToTypeDeserializerTest.java @@ -0,0 +1,123 @@ +package com.fnproject.events.coercion.jackson; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fnproject.events.input.sch.StreamingData; +import com.fnproject.events.testfns.Animal; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; + +public class Base64ToTypeDeserializerTest { + private final ObjectMapper mapper = new ObjectMapper(); + + // Helpers + private static String b64(String s) { + return Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8)); + } + + private static String animalJson(String name, int age) { + return "{\"name\":\"" + name + "\",\"age\":" + age + "}"; + } + + @Test + public void testDecodeBase64JsonIntoPojo() throws Exception { + String valueB64 = b64(animalJson("Felix", 2)); + String json = "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k\", \"value\":\"" + valueB64 + "\", \"offset\":\"o\" }"; + + StreamingData sd = mapper.readValue(json, new TypeReference>() { + }); + assertThat(sd.getValue(), instanceOf(Animal.class)); + assertEquals("Felix", sd.getValue().getName()); + assertEquals(2, sd.getValue().getAge()); + } + + @Test + public void testDecodeBase64TextIntoString() throws Exception { + String valueB64 = b64("hello world"); + String json = "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k\", \"value\":\"" + valueB64 + "\", \"offset\":\"o\" }"; + + StreamingData sd = mapper.readValue(json, new TypeReference>() { + }); + assertEquals("hello world", sd.getValue()); + } + + @Test + public void testPassThroughNonBase64StringForStringTarget() throws Exception { + String json = "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k\", \"value\":\"not-base64$\", \"offset\":\"o\" }"; + + StreamingData sd = mapper.readValue(json, new TypeReference>() { + }); + assertEquals("not-base64$", sd.getValue()); + } + + @Test + public void testInterpretPlainJsonStringWhenNotBase64() throws Exception { + // value is a string that contains JSON; not base64 + String inner = animalJson("Buddy", 1); + String json = "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k\", \"value\":\"" + inner.replace("\"", "\\\"") + "\", \"offset\":\"o\" }"; + + StreamingData sd = mapper.readValue(json, new TypeReference>() { + }); + assertThat(sd.getValue(), instanceOf(Animal.class)); + assertEquals("Buddy", sd.getValue().getName()); + assertEquals(1, sd.getValue().getAge()); + } + + @Test + public void testNonStringValueDelegatesNormally() throws Exception { + // The value is already a JSON object (not a string). The deserializer should delegate to normal binding. + String json = "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k\", \"value\": " + animalJson("Milo", 3) + ", \"offset\":\"o\" }"; + + StreamingData sd = mapper.readValue(json, new TypeReference>() { + }); + assertThat(sd.getValue(), instanceOf(Animal.class)); + assertEquals("Milo", sd.getValue().getName()); + assertEquals(3, sd.getValue().getAge()); + } + + @Test + public void testNullValueReturnsNull() throws Exception { + String json = "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k\", \"value\": null, \"offset\":\"o\" }"; + + StreamingData sd = mapper.readValue(json, new TypeReference>() { + }); + assertNull(sd.getValue()); + } + + @Test + public void testResolveTypeFromEnclosingGenericInCollection() throws Exception { + String v1 = b64(animalJson("Felix", 1)); + String v2 = b64(animalJson("Buddy", 2)); + String json = "[ " + + "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k1\", \"value\":\"" + v1 + "\", \"offset\":\"o1\" }," + + "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k2\", \"value\":\"" + v2 + "\", \"offset\":\"o2\" }" + + "]"; + + List> list = mapper.readValue( + json, new TypeReference>>() { + }); + assertEquals(2, list.size()); + assertThat(list.get(0).getValue(), instanceOf(Animal.class)); + assertEquals("Felix", list.get(0).getValue().getName()); + assertThat(list.get(1).getValue(), instanceOf(Animal.class)); + assertEquals("Buddy", list.get(1).getValue().getName()); + } + + @Test + public void testInvalidNonJsonForNonStringTargetYieldsMismatch() { + // Not base64; not JSON; not coercible to Animal -> should surface a MismatchedInputException + String json = "{ \"stream\":\"s\", \"partition\":\"p\", \"key\":\"k\", \"value\":\"not-json-or-base64\", \"offset\":\"o\" }"; + + assertThrows(MismatchedInputException.class, () -> mapper.readValue(json, new TypeReference>() {})); + } + +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/mapper/APIGatewayRequestEventMapperTest.java b/fn-events/src/test/java/com/fnproject/events/mapper/APIGatewayRequestEventMapperTest.java new file mode 100644 index 00000000..534dc3ac --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/mapper/APIGatewayRequestEventMapperTest.java @@ -0,0 +1,112 @@ +package com.fnproject.events.mapper; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.testfns.Animal; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.QueryParameters; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; +import com.fnproject.fn.runtime.httpgateway.QueryParametersImpl; +import org.junit.Before; +import org.junit.Test; + +public class APIGatewayRequestEventMapperTest { + + private APIGatewayRequestEventMapper mapper; + + @Before + public void setUp() { + mapper = new APIGatewayRequestEventMapper(); + } + + @Test + public void testQueryParameter() { + Map> params = new HashMap<>(); + List paramValues = new ArrayList<>(); + paramValues.add("value"); + paramValues.add("value2"); + params.put("keyTest", paramValues); + QueryParametersImpl qp = new QueryParametersImpl(params); + + HTTPGatewayContext httpGatewayContextMock = mock(HTTPGatewayContext.class); + when(httpGatewayContextMock.getQueryParameters()).thenReturn(qp); + + APIGatewayRequestEvent event = mapper.toApiGatewayRequestEvent(httpGatewayContextMock, ""); + QueryParameters queryParameters = event.getQueryParameters(); + + assertEquals("value", queryParameters.getValues("keyTest").get(0)); + assertEquals("value2", queryParameters.getValues("keyTest").get(1)); + } + + @Test + public void testGetBody() { + String payload = "value"; + + HTTPGatewayContext httpGatewayContextMock = mock(HTTPGatewayContext.class); + APIGatewayRequestEvent event = mapper.toApiGatewayRequestEvent(httpGatewayContextMock, payload); + String body = event.getBody(); + + assertEquals("value", body); + } + + @Test + public void testGetBodyObject() { + Animal request = new Animal("value", 1); + HTTPGatewayContext httpGatewayContextMock = mock(HTTPGatewayContext.class); + APIGatewayRequestEvent event = mapper.toApiGatewayRequestEvent(httpGatewayContextMock, request); + Animal body = event.getBody(); + + assertEquals(request, body); + } + + @Test + public void testGetNullBodyObject() { + HTTPGatewayContext httpGatewayContextMock = mock(HTTPGatewayContext.class); + APIGatewayRequestEvent event = mapper.toApiGatewayRequestEvent(httpGatewayContextMock, null); + Animal body = event.getBody(); + + assertNull(body); + } + + @Test + public void testGetMethod() { + HTTPGatewayContext httpGatewayContextMock = mock(HTTPGatewayContext.class); + when(httpGatewayContextMock.getMethod()).thenReturn("GET"); + APIGatewayRequestEvent event = mapper.toApiGatewayRequestEvent(httpGatewayContextMock, ""); + String method = event.getMethod(); + + assertEquals("GET", method); + } + + @Test + public void testGetHeaders() { + HTTPGatewayContext httpGatewayContextMock = mock(HTTPGatewayContext.class); + + Headers headers = Headers.emptyHeaders().addHeader("key1", "value1"); + when(httpGatewayContextMock.getHeaders()).thenReturn(headers); + + APIGatewayRequestEvent event = mapper.toApiGatewayRequestEvent(httpGatewayContextMock, ""); + Headers eventHeaders = event.getHeaders(); + + assertEquals("value1", eventHeaders.get("key1").get()); + } + + @Test + public void testGetRepeatedHeaders() { + HTTPGatewayContext httpGatewayContextMock = mock(HTTPGatewayContext.class); + Headers headers = Headers.emptyHeaders().addHeader("repeat", "1", "2"); + when(httpGatewayContextMock.getHeaders()).thenReturn(headers); + + APIGatewayRequestEvent event = mapper.toApiGatewayRequestEvent(httpGatewayContextMock, ""); + Headers eventHeaders = event.getHeaders(); + + assertEquals("1", eventHeaders.getAllValues("repeat").get(0)); + assertEquals("2", eventHeaders.getAllValues("repeat").get(1)); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/output/APIGatewayResponseEventTest.java b/fn-events/src/test/java/com/fnproject/events/output/APIGatewayResponseEventTest.java new file mode 100644 index 00000000..f2b83ca8 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/output/APIGatewayResponseEventTest.java @@ -0,0 +1,63 @@ +package com.fnproject.events.output; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import java.util.Collections; +import java.util.List; +import com.fnproject.events.testfns.Animal; +import com.fnproject.fn.api.Headers; +import org.junit.Test; + +public class APIGatewayResponseEventTest { + + @Test + public void testGetBody() { + String body = "body"; + APIGatewayResponseEvent event = new APIGatewayResponseEvent.Builder().body(body).build(); + assertEquals("body", event.getBody()); + } + + @Test + public void testGetBodyAsObject() { + Animal response = new Animal("body", 1); + APIGatewayResponseEvent event = new APIGatewayResponseEvent.Builder().body(response).build(); + assertEquals(response, event.getBody()); + } + + @Test + public void testGetBodyAsList() { + Animal response = new Animal("body", 1); + List list = Collections.singletonList(response); + APIGatewayResponseEvent> event = new APIGatewayResponseEvent.Builder>().body(list).build(); + assertEquals(list, event.getBody()); + } + + @Test + public void testGetNullBody() { + APIGatewayResponseEvent> event = new APIGatewayResponseEvent.Builder>().body(null).build(); + assertNull(event.getBody()); + } + + @Test + public void testGetStatus() { + APIGatewayResponseEvent event = new APIGatewayResponseEvent.Builder().statusCode(201).build(); + assertEquals(Integer.valueOf(201), event.getStatus()); + } + + @Test + public void testGetHeaders() { + Headers headers = Headers.emptyHeaders().addHeader("foo","bar"); + APIGatewayResponseEvent event = new APIGatewayResponseEvent.Builder().headers(headers).build(); + assertEquals(headers, event.getHeaders()); + } + + @Test + public void testGetRepeatedHeaders() { + Headers headers = Headers.emptyHeaders().addHeader("repeated", "foo").addHeader("repeated", "bar"); + APIGatewayResponseEvent event = new APIGatewayResponseEvent.Builder() + .headers(headers) + .build(); + assertEquals("foo", event.getHeaders().getAllValues("repeated").get(0)); + assertEquals("bar", event.getHeaders().getAllValues("repeated").get(1)); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/Animal.java b/fn-events/src/test/java/com/fnproject/events/testfns/Animal.java new file mode 100644 index 00000000..0a82911c --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/Animal.java @@ -0,0 +1,50 @@ +package com.fnproject.events.testfns; + +import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Animal { + private final String name; + private final int age; + + @JsonCreator + public Animal(@JsonProperty("name") String name, + @JsonProperty("age") int age) { + this.name = name; + this.age = age; + } + + public int getAge() { + return age; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Animal animal = (Animal) o; + return age == animal.age && Objects.equals(name, animal.name); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + + @Override + public String toString() { + return "Animal{" + + "name='" + name + '\'' + + ", age=" + age + + '}'; + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/Car.java b/fn-events/src/test/java/com/fnproject/events/testfns/Car.java new file mode 100644 index 00000000..7ff1efe4 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/Car.java @@ -0,0 +1,24 @@ +package com.fnproject.events.testfns; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class Car { + private final String brand; + private final int wheels; + + @JsonCreator + public Car(@JsonProperty("brand") String brand, + @JsonProperty("wheels") int wheels) { + this.brand = brand; + this.wheels = wheels; + } + + public int getWheels() { + return wheels; + } + + public String getBrand() { + return brand; + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/APIGatewayTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/APIGatewayTestFunction.java new file mode 100644 index 00000000..64c8edb3 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/APIGatewayTestFunction.java @@ -0,0 +1,16 @@ +package com.fnproject.events.testfns.apigatewayfns; + +import com.fnproject.events.APIGatewayFunction; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.events.testfns.Animal; +import com.fnproject.events.testfns.Car; + +public class APIGatewayTestFunction extends APIGatewayFunction { + @Override + public APIGatewayResponseEvent handler(APIGatewayRequestEvent requestEvent) { + return new APIGatewayResponseEvent.Builder() + .body(new Car("ford", 4)) + .build(); + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/GrandChildGatewayTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/GrandChildGatewayTestFunction.java new file mode 100644 index 00000000..3364fd32 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/GrandChildGatewayTestFunction.java @@ -0,0 +1,11 @@ +package com.fnproject.events.testfns.apigatewayfns; + +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; + +public class GrandChildGatewayTestFunction extends StringAPIGatewayTestFunction { + @Override + public APIGatewayResponseEvent handler(APIGatewayRequestEvent requestEvent) { + return super.handler(requestEvent); + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/ListAPIGatewayTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/ListAPIGatewayTestFunction.java new file mode 100644 index 00000000..b1b5d0d2 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/ListAPIGatewayTestFunction.java @@ -0,0 +1,18 @@ +package com.fnproject.events.testfns.apigatewayfns; + +import java.util.Collections; +import java.util.List; +import com.fnproject.events.APIGatewayFunction; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; +import com.fnproject.events.testfns.Animal; +import com.fnproject.events.testfns.Car; + +public class ListAPIGatewayTestFunction extends APIGatewayFunction, List> { + @Override + public APIGatewayResponseEvent> handler(APIGatewayRequestEvent> requestEvent) { + return new APIGatewayResponseEvent.Builder>() + .body(Collections.singletonList(new Car("ford", 4))) + .build(); + } +} \ No newline at end of file diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/StringAPIGatewayTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/StringAPIGatewayTestFunction.java new file mode 100644 index 00000000..96c35f58 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/StringAPIGatewayTestFunction.java @@ -0,0 +1,16 @@ +package com.fnproject.events.testfns.apigatewayfns; + +import com.fnproject.events.APIGatewayFunction; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; + +public class StringAPIGatewayTestFunction extends APIGatewayFunction { + @Override + public APIGatewayResponseEvent handler(APIGatewayRequestEvent requestEvent) { + return new APIGatewayResponseEvent.Builder() + .body("test response") + .statusCode(200) + .headers(requestEvent.getHeaders()) + .build(); + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/UncheckedAPIGatewayTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/UncheckedAPIGatewayTestFunction.java new file mode 100644 index 00000000..7d2912b6 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/apigatewayfns/UncheckedAPIGatewayTestFunction.java @@ -0,0 +1,14 @@ +package com.fnproject.events.testfns.apigatewayfns; + +import com.fnproject.events.APIGatewayFunction; +import com.fnproject.events.input.APIGatewayRequestEvent; +import com.fnproject.events.output.APIGatewayResponseEvent; + +public class UncheckedAPIGatewayTestFunction extends APIGatewayFunction { + @Override + public APIGatewayResponseEvent handler(APIGatewayRequestEvent requestEvent) { + return new APIGatewayResponseEvent.Builder() + .body("test response") + .build(); + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/GrandChildMonitorSourceTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/GrandChildMonitorSourceTestFunction.java new file mode 100644 index 00000000..224d8182 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/GrandChildMonitorSourceTestFunction.java @@ -0,0 +1,12 @@ +package com.fnproject.events.testfns.connectorhub; + +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.MetricData; + +public class GrandChildMonitorSourceTestFunction extends MonitorSourceTestFunction { + + @Override + public void handler(ConnectorHubBatch batch) { + super.handler(batch); + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/LoggingSourceTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/LoggingSourceTestFunction.java new file mode 100644 index 00000000..de8ffb84 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/LoggingSourceTestFunction.java @@ -0,0 +1,13 @@ +package com.fnproject.events.testfns.connectorhub; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.LoggingData; + +public class LoggingSourceTestFunction extends ConnectorHubFunction { + + @Override + public void handler(ConnectorHubBatch batch) { + + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/MonitorSourceTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/MonitorSourceTestFunction.java new file mode 100644 index 00000000..5629204e --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/MonitorSourceTestFunction.java @@ -0,0 +1,13 @@ +package com.fnproject.events.testfns.connectorhub; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.MetricData; + +public class MonitorSourceTestFunction extends ConnectorHubFunction { + + @Override + public void handler(ConnectorHubBatch batch) { + + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/QueueSourceObjectTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/QueueSourceObjectTestFunction.java new file mode 100644 index 00000000..99b43583 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/QueueSourceObjectTestFunction.java @@ -0,0 +1,13 @@ +package com.fnproject.events.testfns.connectorhub; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.testfns.Animal; + +public class QueueSourceObjectTestFunction extends ConnectorHubFunction { + + @Override + public void handler(ConnectorHubBatch batch) { + + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/QueueSourceStringTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/QueueSourceStringTestFunction.java new file mode 100644 index 00000000..e3d82f00 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/QueueSourceStringTestFunction.java @@ -0,0 +1,12 @@ +package com.fnproject.events.testfns.connectorhub; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; + +public class QueueSourceStringTestFunction extends ConnectorHubFunction { + + @Override + public void handler(ConnectorHubBatch batch) { + + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/StreamingSourceObjectTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/StreamingSourceObjectTestFunction.java new file mode 100644 index 00000000..c1527d7f --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/StreamingSourceObjectTestFunction.java @@ -0,0 +1,14 @@ +package com.fnproject.events.testfns.connectorhub; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.StreamingData; +import com.fnproject.events.testfns.Animal; + +public class StreamingSourceObjectTestFunction extends ConnectorHubFunction> { + + @Override + public void handler(ConnectorHubBatch> batch) { + + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/StreamingSourceStringTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/StreamingSourceStringTestFunction.java new file mode 100644 index 00000000..dfe2bdd1 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/connectorhub/StreamingSourceStringTestFunction.java @@ -0,0 +1,14 @@ +package com.fnproject.events.testfns.connectorhub; + +import com.fnproject.events.ConnectorHubFunction; +import com.fnproject.events.input.ConnectorHubBatch; +import com.fnproject.events.input.sch.StreamingData; +import com.fnproject.events.testfns.Animal; + +public class StreamingSourceStringTestFunction extends ConnectorHubFunction> { + + @Override + public void handler(ConnectorHubBatch> batch) { + + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/notification/NotificationObjectTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/notification/NotificationObjectTestFunction.java new file mode 100644 index 00000000..54c0f873 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/notification/NotificationObjectTestFunction.java @@ -0,0 +1,13 @@ +package com.fnproject.events.testfns.notification; + +import com.fnproject.events.NotificationFunction; +import com.fnproject.events.input.NotificationMessage; +import com.fnproject.events.testfns.Animal; + +public class NotificationObjectTestFunction extends NotificationFunction { + + @Override + public void handler(NotificationMessage batch) { + + } +} diff --git a/fn-events/src/test/java/com/fnproject/events/testfns/notification/NotificationStringTestFunction.java b/fn-events/src/test/java/com/fnproject/events/testfns/notification/NotificationStringTestFunction.java new file mode 100644 index 00000000..18a94eb9 --- /dev/null +++ b/fn-events/src/test/java/com/fnproject/events/testfns/notification/NotificationStringTestFunction.java @@ -0,0 +1,12 @@ +package com.fnproject.events.testfns.notification; + +import com.fnproject.events.NotificationFunction; +import com.fnproject.events.input.NotificationMessage; + +public class NotificationStringTestFunction extends NotificationFunction { + + @Override + public void handler(NotificationMessage batch) { + + } +} diff --git a/fn-spring-cloud-function/pom.xml b/fn-spring-cloud-function/pom.xml index 97130c81..ed84d672 100644 --- a/fn-spring-cloud-function/pom.xml +++ b/fn-spring-cloud-function/pom.xml @@ -1,4 +1,22 @@ + + @@ -8,12 +26,11 @@ 1.0.0-SNAPSHOT 4.0.0 - + fn-spring-cloud-function fn-spring-cloud-function - UTF-8 - 1.0.0.M1 + 3.2.3 @@ -21,7 +38,6 @@ com.fnproject.fn api - ${project.version} @@ -33,47 +49,52 @@ io.projectreactor reactor-core - 3.0.7.RELEASE + 3.4.10 net.jodah typetools - 0.5.0 com.fnproject.fn - testing - ${project.version} + testing-core + test + + + com.fnproject.fn + testing-junit4 test - org.mockito mockito-core - ${mockito.version} test org.assertj assertj-core - ${assertj-core.version} test junit junit - ${junit.version} + test + + + + commons-logging + commons-logging test com.github.stefanbirkner system-rules - 1.16.0 + 1.19.0 test @@ -98,4 +119,21 @@ + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SimpleTypeWrapper.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SimpleTypeWrapper.java index 1c0e949e..178490e4 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SimpleTypeWrapper.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SimpleTypeWrapper.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function; import com.fnproject.fn.api.TypeWrapper; diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionFeature.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionFeature.java new file mode 100644 index 00000000..04638028 --- /dev/null +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionFeature.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.springframework.function; + +import com.fnproject.fn.api.FunctionInvoker; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.RuntimeFeature; + +/** + * + * The SpringCloudFunctionFeature enables a function to be run with a spring cloud function configuration + * + * Created on 10/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class SpringCloudFunctionFeature implements RuntimeFeature { + + @Override + public void initialize(RuntimeContext ctx) { + ctx.addInvoker(new SpringCloudFunctionInvoker(ctx.getMethod().getTargetClass()),FunctionInvoker.Phase.Call); + } +} diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java index bca11b33..c7de738d 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionInvoker.java @@ -1,20 +1,33 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function; -import com.fnproject.fn.api.FunctionInvoker; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.InvocationContext; -import com.fnproject.fn.api.MethodWrapper; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.*; import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.exception.FunctionOutputHandlingException; import com.fnproject.springframework.function.functions.SpringCloudMethod; + +import org.springframework.boot.Banner; +import org.springframework.boot.WebApplicationType; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.context.ConfigurableApplicationContext; import reactor.core.publisher.Flux; import java.io.Closeable; -import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -42,7 +55,7 @@ public class SpringCloudFunctionInvoker implements FunctionInvoker, Closeable { */ public SpringCloudFunctionInvoker(Class configClass) { SpringApplicationBuilder builder = new SpringApplicationBuilder(configClass); - applicationContext = builder.web(false).run(); + applicationContext = builder.web(WebApplicationType.NONE).bannerMode(Banner.Mode.OFF).run(); loader = applicationContext.getAutowireCapableBeanFactory().createBean(SpringCloudFunctionLoader.class); loader.loadFunction(); } @@ -145,7 +158,7 @@ private Flux convertToFlux(Object[] params) { } @Override - public void close() throws IOException { + public void close() { applicationContext.close(); } } diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java index 5090a595..ae0f9a6d 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/SpringCloudFunctionLoader.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function; import com.fnproject.springframework.function.exception.SpringCloudFunctionNotFoundException; @@ -6,8 +22,8 @@ import com.fnproject.springframework.function.functions.SpringCloudMethod; import com.fnproject.springframework.function.functions.SpringCloudSupplier; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.function.context.FunctionInspector; -import org.springframework.cloud.function.core.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; import reactor.core.publisher.Flux; import java.util.function.Consumer; @@ -18,7 +34,7 @@ * The Loader for Spring Cloud Functions *

* Looks up Functions from the {@link FunctionCatalog} (which is likely populated by your - * function class, see {@link SpringCloudFunctionInvoker#SpringCloudFunctionInvoker(Class)}) + * function class, see {@link SpringCloudFunctionInvoker#SpringCloudFunctionInvoker(SpringCloudFunctionLoader)}) *

* Lookup is in the following order: *

@@ -38,16 +54,14 @@ public class SpringCloudFunctionLoader { public static final String ENV_VAR_CONSUMER_NAME = "FN_SPRING_CONSUMER"; public static final String ENV_VAR_SUPPLIER_NAME = "FN_SPRING_SUPPLIER"; - private final FunctionCatalog catalog; - private final FunctionInspector inspector; + private final SimpleFunctionRegistry registry; private Function, Flux> function; private Consumer> consumer; private Supplier> supplier; - SpringCloudFunctionLoader(@Autowired FunctionCatalog catalog, @Autowired FunctionInspector inspector) { - this.catalog = catalog; - this.inspector = inspector; + SpringCloudFunctionLoader(@Autowired SimpleFunctionRegistry registry) { + this.registry = registry; } void loadFunction() { @@ -67,24 +81,24 @@ void loadFunction() { private void loadSpringCloudFunctionFromEnvVars() { String functionName = System.getenv(ENV_VAR_FUNCTION_NAME); if (functionName != null) { - function = this.catalog.lookupFunction(functionName); + function = this.registry.lookup(Function.class, functionName); } String consumerName = System.getenv(ENV_VAR_CONSUMER_NAME); if (consumerName != null) { - consumer = this.catalog.lookupConsumer(consumerName); + consumer = this.registry.lookup(Consumer.class, consumerName); } String supplierName = System.getenv(ENV_VAR_SUPPLIER_NAME); if (supplierName != null) { - supplier = this.catalog.lookupSupplier(supplierName); + supplier = this.registry.lookup(Supplier.class, supplierName); } } private void loadSpringCloudFunctionFromDefaults() { - function = this.catalog.lookupFunction(DEFAULT_FUNCTION_BEAN); - consumer = this.catalog.lookupConsumer(DEFAULT_CONSUMER_BEAN); - supplier = this.catalog.lookupSupplier(DEFAULT_SUPPLIER_BEAN); + function = this.registry.lookup(Function.class, DEFAULT_FUNCTION_BEAN); + consumer = this.registry.lookup(Consumer.class, DEFAULT_CONSUMER_BEAN); + supplier = this.registry.lookup(Supplier.class, DEFAULT_SUPPLIER_BEAN); } @@ -95,11 +109,11 @@ private boolean noSpringCloudFunctionFound() { SpringCloudMethod getFunction() { if (function != null) { - return new SpringCloudFunction(function, inspector); + return new SpringCloudFunction(function, registry); } else if (consumer != null) { - return new SpringCloudConsumer(consumer, inspector); + return new SpringCloudConsumer(consumer, registry); } else { - return new SpringCloudSupplier(supplier, inspector); + return new SpringCloudSupplier(supplier, registry); } } } diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/exception/SpringCloudFunctionNotFoundException.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/exception/SpringCloudFunctionNotFoundException.java index 37c6dba7..5c5d8504 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/exception/SpringCloudFunctionNotFoundException.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/exception/SpringCloudFunctionNotFoundException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function.exception; import com.fnproject.fn.api.exception.FunctionLoadException; diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java index bc702a16..bc16a9bb 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudConsumer.java @@ -1,8 +1,24 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function.functions; import com.fnproject.fn.api.TypeWrapper; import com.fnproject.springframework.function.SimpleTypeWrapper; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; import reactor.core.publisher.Flux; import java.util.function.Consumer; @@ -13,8 +29,8 @@ public class SpringCloudConsumer extends SpringCloudMethod { private Consumer> consumer; - public SpringCloudConsumer(Consumer> consumer, FunctionInspector inspector) { - super(inspector); + public SpringCloudConsumer(Consumer> consumer, SimpleFunctionRegistry registry) { + super(registry); this.consumer = consumer; } diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java index e7cc1bd5..48d99186 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudFunction.java @@ -1,9 +1,24 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function.functions; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; import reactor.core.publisher.Flux; -import java.util.function.Consumer; import java.util.function.Function; /** @@ -12,8 +27,8 @@ public class SpringCloudFunction extends SpringCloudMethod { private Function, Flux> function; - public SpringCloudFunction(Function, Flux> function, FunctionInspector inspector) { - super(inspector); + public SpringCloudFunction(Function, Flux> function, SimpleFunctionRegistry registry) { + super(registry); this.function = function; } diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java index 77f8bb08..3813fcd5 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudMethod.java @@ -1,9 +1,25 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function.functions; import com.fnproject.fn.api.MethodWrapper; import com.fnproject.fn.api.TypeWrapper; import com.fnproject.springframework.function.SimpleTypeWrapper; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; import reactor.core.publisher.Flux; import java.lang.reflect.Method; @@ -15,10 +31,10 @@ * Spring Cloud Functions */ public abstract class SpringCloudMethod implements MethodWrapper { - private FunctionInspector inspector; + private SimpleFunctionRegistry registry; - SpringCloudMethod(FunctionInspector inspector) { - this.inspector = inspector; + SpringCloudMethod(SimpleFunctionRegistry registry) { + this.registry = registry; } @Override @@ -57,19 +73,19 @@ public Method getTargetMethod() { @Override public TypeWrapper getParamType(int index) { - return new SimpleTypeWrapper(inspector.getInputType(getFunction())); + return new SimpleTypeWrapper(registry.getInputType(getFunction())); } @Override public TypeWrapper getReturnType() { - return new SimpleTypeWrapper(inspector.getOutputType(getFunction())); + return new SimpleTypeWrapper(registry.getOutputType(getFunction())); } /** * Invoke the target function object * - * @param arg - * @return + * @param arg fuction invoke arguments + * @return Flux type object */ public abstract Flux invoke(Flux arg); } diff --git a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java index 182d708b..8cf498f2 100644 --- a/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java +++ b/fn-spring-cloud-function/src/main/java/com/fnproject/springframework/function/functions/SpringCloudSupplier.java @@ -1,11 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function.functions; import com.fnproject.fn.api.TypeWrapper; import com.fnproject.springframework.function.SimpleTypeWrapper; -import org.springframework.cloud.function.context.FunctionInspector; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; import reactor.core.publisher.Flux; -import java.util.function.Consumer; import java.util.function.Supplier; /** @@ -14,8 +29,8 @@ public class SpringCloudSupplier extends SpringCloudMethod { private Supplier> supplier; - public SpringCloudSupplier(Supplier> supplier, FunctionInspector inspector) { - super(inspector); + public SpringCloudSupplier(Supplier> supplier, SimpleFunctionRegistry registry) { + super(registry); this.supplier = supplier; } diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SimpleFunctionInspector.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SimpleFunctionInspector.java deleted file mode 100644 index 22e301e9..00000000 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SimpleFunctionInspector.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.fnproject.springframework.function; - -import net.jodah.typetools.TypeResolver; -import org.springframework.cloud.function.context.FunctionInspector; - -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -public class SimpleFunctionInspector implements FunctionInspector { - @Override - public boolean isMessage(Object function) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public Class getInputType(Object function) { - if (function instanceof Function) { - Class[] types = TypeResolver.resolveRawArguments(Function.class, function.getClass()); - return types[0]; - } else if (function instanceof Consumer) { - Class[] types = TypeResolver.resolveRawArguments(Consumer.class, function.getClass()); - return types[0]; - } else if (function instanceof Supplier) { - return Void.class; - } else { - throw new IllegalStateException("You cannot get the input type of a function that doesn't implement one of the java.util.function interfaces"); - } - } - - @Override - public Class getOutputType(Object function) { - if (function instanceof Function) { - Class[] types = TypeResolver.resolveRawArguments(Function.class, function.getClass()); - return types[0]; - } else if (function instanceof Consumer) { - return Void.class; - } else if (function instanceof Supplier) { - Class[] types = TypeResolver.resolveRawArguments(Consumer.class, function.getClass()); - return types[0]; - } else { - throw new IllegalStateException("You cannot get the output type of a function that doesn't implement one of the java.util.function interfaces"); - } - } - - @Override - public Class getInputWrapper(Object function) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public Class getOutputWrapper(Object function) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public Object convert(Object function, String value) { - throw new IllegalStateException("Not implemented"); - } - - @Override - public String getName(Object function) { - return function.toString(); - } -} diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java index 131e77ef..f2c0054d 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerIntegrationTest.java @@ -1,14 +1,29 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function; import com.fnproject.fn.testing.FnTestingRule; import com.fnproject.springframework.function.testfns.EmptyFunctionConfig; import com.fnproject.springframework.function.testfns.FunctionConfig; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.contrib.java.lang.system.EnvironmentVariables; -import java.io.IOException; - import static org.assertj.core.api.Assertions.assertThat; @@ -21,7 +36,7 @@ public class SpringCloudFunctionInvokerIntegrationTest { public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); @Test - public void shouldInvokeFunction() throws IOException { + public void shouldInvokeFunction() { fnRule.givenEvent().withBody("HELLO").enqueue(); fnRule.thenRun(FunctionConfig.class, "handleRequest"); @@ -30,18 +45,23 @@ public void shouldInvokeFunction() throws IOException { } @Test - public void shouldInvokeConsumer() throws IOException { + @Ignore("Consumer behaviour seems broken in this release of Spring Cloud Function") + // NB the problem is that FluxConsumer is not a subclass of j.u.f.Consumer, but _is_ + // a subclass of j.u.f.Function. + // Effectively a Consumer is treated as a Function which means when we lookup + // by env var name "consumer", we don't find a j.u.f.Consumer, so we fall back to the default + // behaviour which is to invoke the bean named "function" + public void shouldInvokeConsumer() { environmentVariables.set(SpringCloudFunctionLoader.ENV_VAR_CONSUMER_NAME, "consumer"); - String consumerInput = "consumer input"; - fnRule.givenEvent().withBody(consumerInput).enqueue(); + fnRule.givenEvent().withBody("consumer input").enqueue(); fnRule.thenRun(FunctionConfig.class, "handleRequest"); - assertThat(fnRule.getStdErrAsString()).contains(consumerInput); + assertThat(fnRule.getStdErrAsString()).contains("consumer input"); } @Test - public void shouldInvokeSupplier() throws IOException { + public void shouldInvokeSupplier() { environmentVariables.set(SpringCloudFunctionLoader.ENV_VAR_SUPPLIER_NAME, "supplier"); fnRule.givenEvent().enqueue(); @@ -59,7 +79,7 @@ public void shouldThrowFunctionLoadExceptionIfNoValidFunction() { int exitCode = fnRule.getLastExitCode(); - assertThat(exitCode).isEqualTo(2); + assertThat(exitCode).isEqualTo(1); assertThat(fnRule.getResults()).isEmpty(); // fails at init so no results. assertThat(fnRule.getStdErrAsString()).contains("No Spring Cloud Function found"); } @@ -72,7 +92,7 @@ public void noNPEifFunctionReturnsNull() { int exitCode = fnRule.getLastExitCode(); - assertThat(exitCode).isEqualTo(2); + assertThat(exitCode).isEqualTo(1); assertThat(fnRule.getResults()).isEmpty(); // fails at init so no results. assertThat(fnRule.getStdErrAsString()).contains("No Spring Cloud Function found"); } diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerTest.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerTest.java index 0ed3b1fa..6c708f69 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerTest.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionInvokerTest.java @@ -1,8 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function; import com.fnproject.springframework.function.functions.SpringCloudFunction; import org.junit.Before; import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; import java.util.Arrays; import java.util.List; @@ -12,6 +30,9 @@ public class SpringCloudFunctionInvokerTest { private SpringCloudFunctionInvoker invoker; + @Autowired + private SimpleFunctionRegistry registry; + @Before public void setUp() { invoker = new SpringCloudFunctionInvoker((SpringCloudFunctionLoader) null); @@ -19,7 +40,7 @@ public void setUp() { @Test public void invokesFunctionWithEmptyFlux() { - SpringCloudFunction fnWrapper = new SpringCloudFunction(x -> x, new SimpleFunctionInspector()); + SpringCloudFunction fnWrapper = new SpringCloudFunction(x -> x, registry); Object result = invoker.tryInvoke(fnWrapper, new Object[0]); @@ -28,7 +49,7 @@ public void invokesFunctionWithEmptyFlux() { @Test public void invokesFunctionWithFluxOfSingleItem() { - SpringCloudFunction fnWrapper = new SpringCloudFunction(x -> x, new SimpleFunctionInspector()); + SpringCloudFunction fnWrapper = new SpringCloudFunction(x -> x, registry); Object result = invoker.tryInvoke(fnWrapper, new Object[]{ "hello" }); @@ -38,7 +59,7 @@ public void invokesFunctionWithFluxOfSingleItem() { @Test public void invokesFunctionWithFluxOfMultipleItems() { - SpringCloudFunction fnWrapper = new SpringCloudFunction(x -> x, new SimpleFunctionInspector()); + SpringCloudFunction fnWrapper = new SpringCloudFunction(x -> x, registry); Object result = invoker.tryInvoke(fnWrapper, new Object[]{ Arrays.asList("hello", "world") }); diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java index 9ce1049c..2be39d5b 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/SpringCloudFunctionLoaderTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function; import com.fnproject.springframework.function.functions.SpringCloudMethod; @@ -8,14 +24,13 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.cloud.function.core.FunctionCatalog; +import org.springframework.cloud.function.context.catalog.BeanFactoryAwareFunctionRegistry; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) @@ -24,19 +39,13 @@ public class SpringCloudFunctionLoaderTest { public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); private SpringCloudFunctionLoader loader; + @Mock - private FunctionCatalog catalog; + private BeanFactoryAwareFunctionRegistry registry; @Before public void setUp() { - loader = new SpringCloudFunctionLoader(catalog, null); - setUpCatalogToReturnNullForLookupByDefault(); - } - - private void setUpCatalogToReturnNullForLookupByDefault() { - when(catalog.lookupFunction(any())).thenReturn(null); - when(catalog.lookupConsumer(any())).thenReturn(null); - when(catalog.lookupSupplier(any())).thenReturn(null); + loader = new SpringCloudFunctionLoader(registry); } @Test @@ -49,7 +58,8 @@ public void shouldLoadFunctionBeanCalledFunction() { @Test public void shouldLoadConsumerBeanCalledConsumerIfFunctionNotAvailable() { - Consumer consumer = (x) -> {}; + Consumer consumer = (x) -> { + }; stubCatalogToReturnConsumer(consumer); assertThat(getDiscoveredFunction().getTargetClass()).isEqualTo(consumer.getClass()); @@ -80,7 +90,8 @@ public void shouldLoadUserSpecifiedSupplierInEnvVarOverDefaultFunction() { @Test public void shouldLoadUserSpecifiedConsumerInEnvVarOverDefaultFunction() { String beanName = "myConsumer"; - Consumer consumer = (x) -> {}; + Consumer consumer = (x) -> { + }; Function function = (x) -> x; setConsumerEnvVar(beanName); @@ -104,15 +115,15 @@ public void shouldLoadUserSpecifiedFunctionInEnvVarOverDefaultFunction() { } private void stubCatalogToReturnFunction(String beanName, Function function) { - when(catalog.lookupFunction(beanName)).thenReturn(function); + when(registry.lookup(Function.class, beanName)).thenReturn(function); } private void stubCatalogToReturnConsumer(String beanName, Consumer consumer) { - when(catalog.lookupConsumer(beanName)).thenReturn(consumer); + when(registry.lookup(Consumer.class, beanName)).thenReturn(consumer); } private void stubCatalogToReturnSupplier(String beanName, Supplier supplier) { - when(catalog.lookupSupplier(beanName)).thenReturn(supplier); + when(registry.lookup(Supplier.class, beanName)).thenReturn(supplier); } private void stubCatalogToReturnSupplier(Supplier supplier) { diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java index a6444bc2..50b8b09c 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/EmptyFunctionConfig.java @@ -1,24 +1,37 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function.testfns; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.FnFeature; +import com.fnproject.fn.api.FunctionInvoker; import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.springframework.function.SpringCloudFunctionFeature; import com.fnproject.springframework.function.SpringCloudFunctionInvoker; -import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration; -import org.springframework.context.annotation.Bean; +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - @Configuration @Import(ContextFunctionCatalogAutoConfiguration.class) +@FnFeature(SpringCloudFunctionFeature.class) public class EmptyFunctionConfig { - @FnConfiguration - public static void configure(RuntimeContext ctx) { - ctx.setInvoker(new SpringCloudFunctionInvoker(EmptyFunctionConfig.class)); + + public void handleRequest() { } - public void handleRequest() { } } diff --git a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java index c5d4d21a..8c76e911 100644 --- a/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java +++ b/fn-spring-cloud-function/src/test/java/com/fnproject/springframework/function/testfns/FunctionConfig.java @@ -1,12 +1,31 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.springframework.function.testfns; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.FnFeature; import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.springframework.function.SpringCloudFunctionFeature; import com.fnproject.springframework.function.SpringCloudFunctionInvoker; -import org.springframework.cloud.function.context.ContextFunctionCatalogAutoConfiguration; +import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import reactor.core.publisher.Flux; import java.util.function.Consumer; import java.util.function.Function; @@ -14,20 +33,21 @@ @Configuration @Import(ContextFunctionCatalogAutoConfiguration.class) +@FnFeature(SpringCloudFunctionFeature.class) public class FunctionConfig { - @FnConfiguration - public static void configure(RuntimeContext ctx) { - ctx.setInvoker(new SpringCloudFunctionInvoker(FunctionConfig.class)); + + public void handleRequest() { } - public void handleRequest() { } @Bean - public Supplier supplier() { - return () -> "Hello"; + public Supplier> supplier() { + String str = "Hello"; + return () -> Flux.just(str); } @Bean public Consumer consumer() { + System.out.println("LOADED"); return System.out::println; } @@ -35,13 +55,16 @@ public Consumer consumer() { public Function function() { return String::toLowerCase; } + @Bean public Function upperCaseFunction() { return String::toUpperCase; } @Bean - public String notAFunction() { return "NotAFunction"; } + public String notAFunction() { + return "NotAFunction"; + } // Empty entrypoint that isn't used but necessary for the EntryPoint. Our invoker ignores this and loads our own // function to invoke diff --git a/graalvm.version b/graalvm.version new file mode 100644 index 00000000..90b68074 --- /dev/null +++ b/graalvm.version @@ -0,0 +1 @@ +21.0.0.2 diff --git a/infra/provision/Jenkinsfile b/infra/provision/Jenkinsfile deleted file mode 100644 index 98d1f423..00000000 --- a/infra/provision/Jenkinsfile +++ /dev/null @@ -1,35 +0,0 @@ -pipeline { - agent any - - stages { - stage ('Get the infra files from functions-service') { - steps { - sh ". /home/mjg/proxy ; rm -rf functions-service ; git clone git@gitlab-odx.oracle.com:odx/functions-service.git functions-service" - } - } - stage ('Validate the infra files from functions-service so we do not get surprises when running them for real') { - steps { - sh ". /home/mjg/proxy ; infra/provision/validate-functions-files.sh functions-service" - } - } - stage ('Delete existing k8s entities') { - steps { - sh ". /home/mjg/proxy ; infra/provision/delete-k8s-entities.sh infra/provision functions-service" - } - } - stage ('Provision the whole infrastructure') { - steps { - sh ". /home/mjg/proxy ; infra/provision/create-k8s-entities.sh infra/provision functions-service" - } - } - } - - post { - success { - mattermostSend color: 'good', endpoint: 'https://odx.stengpoc.ucfc2z3b.usdv1.oraclecloud.com/hooks/eqxe7mqgrbrqiryu47wnrjzppy', message: ":poodle: :camel: Provisioned dev environment: ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>) :camel: :poodle:" - } - failure { - mattermostSend color: 'danger', endpoint: 'https://odx.stengpoc.ucfc2z3b.usdv1.oraclecloud.com/hooks/eqxe7mqgrbrqiryu47wnrjzppy', message: ":boom: :boom: Failed to provision dev environment: ${env.JOB_NAME} ${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>) :boom: :boom:" - } - } -} diff --git a/infra/provision/completer-integration-environment.yaml b/infra/provision/completer-integration-environment.yaml deleted file mode 100644 index b95b2b15..00000000 --- a/infra/provision/completer-integration-environment.yaml +++ /dev/null @@ -1,174 +0,0 @@ -# This is run once, at cluster creation time. It creates and sets up the -# components of the flow service, and assumes that the functions service -# is already provisioned from: -# -# https://gitlab-odx.oracle.com/odx/functions-service/tree/master/scripts/fn-service.yml -# -# The created service has persistent storage to a single-stage MySQL database. -# -# Updates are done by: -# -# $ kubectl set image deployment/completer-service completer-service=NEW_IMAGE_FULL_PATH -# ---- -apiVersion: v1 -kind: Secret -metadata: - name: completer-mysql-secret -type: Opaque -data: - MYSQL_DATABASE: YWtrYQ== - MYSQL_USER: cXNSaFVNWVo1NXFDdWg4T29lbVo0c2hxQndEYzNo - MYSQL_PASSWORD: eXF2cEhEUFliU3lyWU9VdHVsb08zQThXTWtKVGFKdmIzTnQ3dFVaZkE4NHlFWnBueXlpUEF3 - MYSQL_ROOT_PASSWORD: N1ZSRWlUSFRXMDlQbnhETzRIQUVQWkxhREEwSHZVQ1pBNWZuVDl3akdEd3ExSk5yRmE3TUhn ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: completer-persistence-schema-file -data: - schema-file.sql: | - USE akka; - CREATE TABLE IF NOT EXISTS journal ( - ordering SERIAL, - persistence_id VARCHAR(255) NOT NULL, - sequence_number BIGINT NOT NULL, - deleted BOOLEAN DEFAULT FALSE, - tags VARCHAR(255) DEFAULT NULL, - message BLOB NOT NULL, - PRIMARY KEY(persistence_id, sequence_number) - ); - CREATE TABLE IF NOT EXISTS snapshot ( - persistence_id VARCHAR(255) NOT NULL, - sequence_number BIGINT NOT NULL, - created BIGINT NOT NULL, - snapshot BLOB NOT NULL, - PRIMARY KEY (persistence_id, sequence_number) - ); - ---- -apiVersion: v1 -kind: Service -metadata: - name: completer-persistence-mysql - labels: - app: completer-mysql - role: datastore -spec: - ports: - - port: 3306 - targetPort: 3306 - selector: - app: completer-mysql - role: datastore ---- -apiVersion: apps/v1beta1 -kind: Deployment -metadata: - name: completer-persistence-mysql -spec: - replicas: 1 - template: - metadata: - labels: - app: completer-mysql - role: datastore - spec: - terminationGracePeriodSeconds: 1 - containers: - - name: completer-mysql - image: mysql:5.7 - imagePullPolicy: Always - args: - - "--max-connections=500" - ports: - - containerPort: 3306 - env: - - name: MYSQL_ROOT_PASSWORD - valueFrom: - secretKeyRef: - name: completer-mysql-secret - key: MYSQL_ROOT_PASSWORD - - name: MYSQL_USER - valueFrom: - secretKeyRef: - name: completer-mysql-secret - key: MYSQL_USER - - name: MYSQL_PASSWORD - valueFrom: - secretKeyRef: - name: completer-mysql-secret - key: MYSQL_PASSWORD - - name: MYSQL_DATABASE - valueFrom: - secretKeyRef: - name: completer-mysql-secret - key: MYSQL_DATABASE - volumeMounts: - - mountPath: /docker-entrypoint-initdb.d - name: schema-file - volumes: - - name: schema-file - configMap: - name: completer-persistence-schema-file ---- -apiVersion: apps/v1beta1 # for versions before 1.6.0 use extensions/v1beta1 -kind: Deployment -metadata: - name: completer-service -spec: - replicas: 1 - template: - metadata: - labels: - app: completer-service - spec: - containers: - - name: completer-service - image: registry.oracledx.com/skeppare/cloud-completer:latest - imagePullPolicy: Always - ports: - - containerPort: 8081 - name: http-server - env: - - name: CONFIG_RESOURCE - value: prod.conf - - name: MYSQL_HOST - value: completer-persistence-mysql - - name: MYSQL_USER - valueFrom: - secretKeyRef: - name: completer-mysql-secret - key: MYSQL_USER - - name: MYSQL_PASSWORD - valueFrom: - secretKeyRef: - name: completer-mysql-secret - key: MYSQL_PASSWORD - - name: MYSQL_DATABASE - valueFrom: - secretKeyRef: - name: completer-mysql-secret - key: MYSQL_DATABASE - - name: FN_HOST - value: fn-service - - name: FN_PORT - value: "8080" - imagePullSecrets: - - name: odx-registry-secret ---- -kind: Service -apiVersion: v1 -metadata: - name: completer-service - labels: - app: completer-service -spec: - clusterIP: 10.32.0.66 - ports: - - port: 8081 - targetPort: http-server - nodePort: 31750 - selector: - app: completer-service - type: NodePort diff --git a/infra/provision/create-k8s-entities.sh b/infra/provision/create-k8s-entities.sh deleted file mode 100755 index 291abf11..00000000 --- a/infra/provision/create-k8s-entities.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash -PROVISION_DIR=$1 -FUNCTIONS_SERVICE_DIR=$2 - -set -ex - -if [[ -z "${PROVISION_DIR// }" ]]; then - echo "ERROR: must specify the jfaas/infra/provision directory as the first argument" - exit 2 -fi -if [[ -z "${FUNCTIONS_SERVICE_DIR// }" ]]; then - echo "ERROR: must specify the location of the functions-service local repository as the second argument" - exit 2 -fi - -# HACK! Apparently even the functions guys have issues pulling some SRE images. -# We're just changing them to pause containers because they are only used for -# metrics and we don't need them. -# Also we set the pulling strategy to Always so we can pull the latest image. -sed "s/imagePullPolicy: IfNotPresent/imagePullPolicy: Always/" $FUNCTIONS_SERVICE_DIR/scripts/fn-service.yml -i -sed s/registry.oracledx.com\\/odxsre\\/core-services-statsd-exporter/kubernetes\\/pause/ $FUNCTIONS_SERVICE_DIR/scripts/fn-service.yml -i -sed s/registry.oracledx.com\\/odxsre\\/core-services-prometheus-pusher/kubernetes\\/pause/ $FUNCTIONS_SERVICE_DIR/scripts/fn-service.yml -i -kubectl create -f $FUNCTIONS_SERVICE_DIR/scripts/docker-reg-secret.yml -kubectl create -f $FUNCTIONS_SERVICE_DIR/scripts/zipkin-service.yml -kubectl create -f $FUNCTIONS_SERVICE_DIR/scripts/fn-service.yml - -kubectl create -f $PROVISION_DIR/registry.yaml -sleep 1 -kubectl create -f $PROVISION_DIR/completer-integration-environment.yaml diff --git a/infra/provision/delete-k8s-entities.sh b/infra/provision/delete-k8s-entities.sh deleted file mode 100755 index cef836df..00000000 --- a/infra/provision/delete-k8s-entities.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -PROVISION_DIR=$1 -FUNCTIONS_SERVICE_DIR=$2 - -set -ex - -if [[ -z "${PROVISION_DIR// }" ]]; then - echo "ERROR: must specify the jfaas/infra/provision directory as the first argument" - exit 2 -fi -if [[ -z "${FUNCTIONS_SERVICE_DIR// }" ]]; then - echo "ERROR: must specify the location of the functions-service local repository as the second argument" - exit 2 -fi - -kubectl delete -f $PROVISION_DIR/completer-integration-environment.yaml || true -kubectl delete -f $PROVISION_DIR/registry.yaml || true - -kubectl delete -f $FUNCTIONS_SERVICE_DIR/scripts/fn-service.yml || true -kubectl delete -f $FUNCTIONS_SERVICE_DIR/scripts/zipkin-service.yml || true -kubectl delete -f $FUNCTIONS_SERVICE_DIR/scripts/docker-reg-secret.yml || true diff --git a/infra/provision/registry.yaml b/infra/provision/registry.yaml deleted file mode 100644 index 1bd9cdd0..00000000 --- a/infra/provision/registry.yaml +++ /dev/null @@ -1,22 +0,0 @@ -# This is run once, at cluster creation time and it creates a deployment and a -# service based on the latest functions image. -# -# The created server is configured so that it does NOT have persistent storage. -# It is only intended to be used as a test server and data can be thrown away. -# -# Updates are done by: -# $ kubectl set image deployment/functions-master functions=NEW_IMAGE_FULL_PATH - ---- -# This may or may not be repeatable. Apparently registry.oracledx.com hands -# out the same auth token every time for the same combination of user/pass, -# so this should work, but we have no guarantees. The problem is that if we -# don't create the secret in a yaml file we have to have a manual process to -# create it, which is not ideal. -kind: Secret -apiVersion: v1 -metadata: - name: odx-registry-secret -type: kubernetes.io/dockercfg -data: - .dockercfg: eyJyZWdpc3RyeS5vcmFjbGVkeC5jb20iOnsidXNlcm5hbWUiOiJhZ2VudCIsInBhc3N3b3JkIjoiZ2YyMVx1MDAyNjU0RyIsImVtYWlsIjoiZGFyaW8uZG9taXppb2xpQG9yYWNsZS5jb20iLCJhdXRoIjoiWVdkbGJuUTZaMll5TVNZMU5FYz0ifX0= diff --git a/infra/provision/validate-functions-files.sh b/infra/provision/validate-functions-files.sh deleted file mode 100755 index b997208d..00000000 --- a/infra/provision/validate-functions-files.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -FUNCTIONS_SERVICE_DIR=$1 - -set -ex - -if [[ -z "${FUNCTIONS_SERVICE_DIR// }" ]]; then - echo "ERROR: must specify the location of the functions-service local repository as the argument" - exit 2 -fi - -# This dry run validation is performed so that we don't delete our environment and then fail to recreate it. -kubectl create --dry-run -f $FUNCTIONS_SERVICE_DIR/scripts/docker-reg-secret.yml -kubectl create --dry-run -f $FUNCTIONS_SERVICE_DIR/scripts/zipkin-service.yml -kubectl create --dry-run -f $FUNCTIONS_SERVICE_DIR/scripts/fn-service.yml - diff --git a/infra/update/functions/Jenkinsfile b/infra/update/functions/Jenkinsfile index b5691ecf..7ba6c01f 100644 --- a/infra/update/functions/Jenkinsfile +++ b/infra/update/functions/Jenkinsfile @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + pipeline { agent any parameters { - string(name: 'IMAGE', defaultValue: 'registry.oracledx.com/skeppare/functions-service:latest', description: 'Which image to use (full repository:tag, e.g. fnproject/functions:latest)') + string(name: 'IMAGE', defaultValue: 'registry.oracledx.com/skeppare/functions-service:latest', description: 'Which image to use (full repository:tag, e.g. fnproject/fnserver:latest)') string(name: 'SOURCE_REPO', defaultValue: 'git@github.com:fnproject/fn.git', description: 'Which git repo to use to build the CLI tool') string(name: 'SOURCE_BRANCH', defaultValue: 'master', description: 'Which git repo branch to use to build the CLI tool') } diff --git a/integration-tests/.gitignore b/integration-tests/.gitignore deleted file mode 100644 index 9b3667f0..00000000 --- a/integration-tests/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*/*/actual -*/*/build-output -*/*/success -*/*/failure -*/*/output diff --git a/integration-tests/README.md b/integration-tests/README.md deleted file mode 100644 index 07dde6f2..00000000 --- a/integration-tests/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# Java FDK integration tests - -Integration tests should only be used for: - -* Smoke testing of end-to-end features -* Rough validation of features that rely on interactions between the fn service and flow service - -They should _not_ be used for: - -* Feature testing of fn/fdk features that don't need cross-service features -* extensive feature testing (use unit tests) -* Performance/load testing - -## Creating a new test - -Put main integration tests under main/test- - -the content of a test dir is a a typically a new function (containing func.yaml, pom.xml etc. ) - -create the following files: -* `input` : the input to pass to the deployed function -* `expected` : the verbatim expected result of the function -* `expected.sh` : a shell script that should succeed when the test passed - this is used in place of `expected` -* `config` : A newline seperated list of config variables to set on the function -* `pre-test.sh` a script that is run before the function is called (e.g. to call fn init to check bootstrapping) - - - -# Running locally - -To run locally you will need to deploy the fn artifacts to a local repository: - -(in top-level dir) -```bash -export REPOSITORY_LOCATION=/tmp/staging-repository - -mvn deploy -DaltDeploymentRepository=localStagingDir::default::file://"$REPOSITORY_LOCATION" -``` - -You may also want to/need build local copies of the build images: -```bash -cd build-images -./docker-build.sh -t fnproject/fn-java-fdk-build . -./docker-build.sh -f Dockerfile-jdk9 -t fnproject/fn-java-fdk-build:jdk9-latest . -``` - -and runtime images: -``` -docker build -t fnproject/fn-java-fdk . -docker build -f Dockerfile-jdk9 -t fnproject/fn-java-fdk:jdk9-latest . -``` - -Finally you can run the integration tests: - -```bash -./integration-tests/run-local.sh -``` - -Note that these will update the pom files in the tests - don't check these in! - - -# Running against a remote environment -For running against a remote integration environment, configure - ~/.fn-token - ~/.fn-api-url - ~/.fn-flow-base-url - -and run the `run-remote.sh` script. diff --git a/integration-tests/lib.sh b/integration-tests/lib.sh deleted file mode 100644 index 83c04b49..00000000 --- a/integration-tests/lib.sh +++ /dev/null @@ -1,48 +0,0 @@ -# Bash clean-up junk -- -typeset -a _deferrals -_on_exit() { - set +x - local idx - for (( idx=${#_deferrals[@]}-1 ; idx>=0 ; idx-- )) ; do - set -x - ${_deferrals[idx]} - set +x - done -} -defer() { - _deferrals+=("$*") -} -trap _on_exit EXIT TERM INT -# -- Bash clean-up junk - -wait_for_http() { - ( - set +ex - local i - for i in {1..15}; do - curl --connect-timeout 5 --max-time 2 "$1" && exit - sleep 1 - done - exit 1 - ) -} - -# prefix each line, whilst evaluating -- -prefix_lines() { - local prefix="$1" - local file="$2" - eval awk \''{print "'"$prefix"' " $0}'\' '<<-EOF'$'\n'"$(< "$file")"$'\n'EOF -} -# -- prefix each line, whilst evaluating - - -line() { - echo -------------------------------------------------------------------------------- -} - -error() { - echo "$1" 1>&2 - exit "${2:-1}" -} - -export SCRIPT_DIR="$( cd "$(dirname "$0")" && command pwd )" diff --git a/integration-tests/main/test-1-jdk8/config b/integration-tests/main/test-1-jdk8/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-1-jdk8/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-1-jdk8/expected b/integration-tests/main/test-1-jdk8/expected deleted file mode 100644 index 62f94575..00000000 --- a/integration-tests/main/test-1-jdk8/expected +++ /dev/null @@ -1 +0,0 @@ -6 \ No newline at end of file diff --git a/integration-tests/main/test-1-jdk8/func.yaml b/integration-tests/main/test-1-jdk8/func.yaml deleted file mode 100644 index 5e89d168..00000000 --- a/integration-tests/main/test-1-jdk8/func.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 0.0.1 -runtime: java8 -cmd: com.fnproject.fn.integration.test_1.CompleterFunction::handleRequest -format: http -timeout: 120 diff --git a/integration-tests/main/test-1-jdk8/input b/integration-tests/main/test-1-jdk8/input deleted file mode 100644 index 00750edc..00000000 --- a/integration-tests/main/test-1-jdk8/input +++ /dev/null @@ -1 +0,0 @@ -3 diff --git a/integration-tests/main/test-1-jdk8/pom.xml b/integration-tests/main/test-1-jdk8/pom.xml deleted file mode 100644 index 37f78c46..00000000 --- a/integration-tests/main/test-1-jdk8/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 4.0.0 - - UTF-8 - 1.0.0-SNAPSHOT - - com.fnproject.fn - integration-test-1 - 1.0.0 - - jar - - - - com.fnproject.fn - api - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - - diff --git a/integration-tests/main/test-1-jdk8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java b/integration-tests/main/test-1-jdk8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java deleted file mode 100644 index edd04e86..00000000 --- a/integration-tests/main/test-1-jdk8/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.fnproject.fn.integration.test_1; - -import com.fnproject.fn.api.flow.Flow; -import com.fnproject.fn.api.flow.Flows; - -public class CompleterFunction { - - public Integer handleRequest(String input) { - Flow fl = Flows.currentFlow(); - - return fl.supply(() -> Integer.parseInt(input)) - .thenApply((i) -> i + 3) - .get(); - } -} diff --git a/integration-tests/main/test-1/config b/integration-tests/main/test-1/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-1/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-1/expected b/integration-tests/main/test-1/expected deleted file mode 100644 index 62f94575..00000000 --- a/integration-tests/main/test-1/expected +++ /dev/null @@ -1 +0,0 @@ -6 \ No newline at end of file diff --git a/integration-tests/main/test-1/func.yaml b/integration-tests/main/test-1/func.yaml deleted file mode 100644 index 72fbe623..00000000 --- a/integration-tests/main/test-1/func.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 0.0.1 -runtime: java -cmd: com.fnproject.fn.integration.test_1.CompleterFunction::handleRequest -format: http -timeout: 120 diff --git a/integration-tests/main/test-1/input b/integration-tests/main/test-1/input deleted file mode 100644 index 00750edc..00000000 --- a/integration-tests/main/test-1/input +++ /dev/null @@ -1 +0,0 @@ -3 diff --git a/integration-tests/main/test-1/pom.xml b/integration-tests/main/test-1/pom.xml deleted file mode 100644 index 37f78c46..00000000 --- a/integration-tests/main/test-1/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 4.0.0 - - UTF-8 - 1.0.0-SNAPSHOT - - com.fnproject.fn - integration-test-1 - 1.0.0 - - jar - - - - com.fnproject.fn - api - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - - diff --git a/integration-tests/main/test-1/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java b/integration-tests/main/test-1/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java deleted file mode 100644 index edd04e86..00000000 --- a/integration-tests/main/test-1/src/main/java/com/fnproject/fn/integration/test_1/CompleterFunction.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.fnproject.fn.integration.test_1; - -import com.fnproject.fn.api.flow.Flow; -import com.fnproject.fn.api.flow.Flows; - -public class CompleterFunction { - - public Integer handleRequest(String input) { - Flow fl = Flows.currentFlow(); - - return fl.supply(() -> Integer.parseInt(input)) - .thenApply((i) -> i + 3) - .get(); - } -} diff --git a/integration-tests/main/test-2/config b/integration-tests/main/test-2/config deleted file mode 100644 index 5646b494..00000000 --- a/integration-tests/main/test-2/config +++ /dev/null @@ -1 +0,0 @@ -GREETING=Salutations diff --git a/integration-tests/main/test-2/expected b/integration-tests/main/test-2/expected deleted file mode 100644 index 4082c5fd..00000000 --- a/integration-tests/main/test-2/expected +++ /dev/null @@ -1 +0,0 @@ -Salutations, world! \ No newline at end of file diff --git a/integration-tests/main/test-2/func.yaml b/integration-tests/main/test-2/func.yaml deleted file mode 100644 index 38e48a45..00000000 --- a/integration-tests/main/test-2/func.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: fn-test/test-2 -version: 0.0.1 -runtime: java9 -cmd: com.fnproject.fn.integration.test2.PlainFunction::handleRequest -format: http diff --git a/integration-tests/main/test-2/input b/integration-tests/main/test-2/input deleted file mode 100644 index e69de29b..00000000 diff --git a/integration-tests/main/test-2/pom.xml b/integration-tests/main/test-2/pom.xml deleted file mode 100644 index da270e30..00000000 --- a/integration-tests/main/test-2/pom.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - 4.0.0 - - UTF-8 - 1.0.0-SNAPSHOT - - com.fnproject.fn - integration-test-2 - 1.0.0 - - - - com.fnproject.fn - api - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - - diff --git a/integration-tests/main/test-2/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java b/integration-tests/main/test-2/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java deleted file mode 100644 index 7ff61897..00000000 --- a/integration-tests/main/test-2/src/main/java/com/fnproject/fn/integration/test2/PlainFunction.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.fnproject.fn.integration.test2; - -import com.fnproject.fn.api.*; - -public class PlainFunction { - - private String greeting; - - @FnConfiguration - public void configuration(RuntimeContext ctx) { - System.err.println("Configuration called"); - this.greeting = ctx.getConfigurationByKey("GREETING") - .orElseThrow(() -> new RuntimeException("Greeting must be set")); - } - - public String handleRequest(String input) { - String name = (input == null || input.isEmpty()) ? "world" : input; - - return greeting + ", " + name + "!"; - } -} diff --git a/integration-tests/main/test-2/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java b/integration-tests/main/test-2/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java deleted file mode 100644 index bdc91fd2..00000000 --- a/integration-tests/main/test-2/src/test/java/com/fnproject/fn/integration/test2/PlainFunctionTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.fnproject.fn.integration.test2; - -import com.fnproject.fn.testing.FnResult; -import com.fnproject.fn.testing.FnTestingRule; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -public class PlainFunctionTest { - - @Rule - public final FnTestingRule testing = FnTestingRule.createDefault(); - - @Before - public void setUp() { - testing.setConfig("GREETING", "Howdy"); - } - - @Test - public void shouldReturnGreeting() { - testing.givenEvent().enqueue(); - testing.thenRun(PlainFunction.class, "handleRequest"); - - FnResult result = testing.getOnlyResult(); - assertEquals("Howdy, world!", result.getBodyAsString()); - } - -} diff --git a/integration-tests/main/test-3/.gitignore b/integration-tests/main/test-3/.gitignore deleted file mode 100644 index a9c2e6bb..00000000 --- a/integration-tests/main/test-3/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/func.yaml -/pom.xml -/src/ diff --git a/integration-tests/main/test-3/expected b/integration-tests/main/test-3/expected deleted file mode 100644 index fda590c7..00000000 --- a/integration-tests/main/test-3/expected +++ /dev/null @@ -1 +0,0 @@ -Hello, function! \ No newline at end of file diff --git a/integration-tests/main/test-3/input b/integration-tests/main/test-3/input deleted file mode 100644 index e2dbde09..00000000 --- a/integration-tests/main/test-3/input +++ /dev/null @@ -1 +0,0 @@ -function diff --git a/integration-tests/main/test-3/pre-test.sh b/integration-tests/main/test-3/pre-test.sh deleted file mode 100755 index 40f613f4..00000000 --- a/integration-tests/main/test-3/pre-test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -rm -rf Dockerfile func.yaml pom.xml src -FN_JAVA_FDK_VERSION=$(cat ../../../release.version) fn init --runtime=java9 --name app/test diff --git a/integration-tests/main/test-4/Dockerfile.custom b/integration-tests/main/test-4/Dockerfile.custom deleted file mode 100644 index e9077432..00000000 --- a/integration-tests/main/test-4/Dockerfile.custom +++ /dev/null @@ -1,4 +0,0 @@ -FROM fnproject/fn-java-fdk:jdk9-latest -WORKDIR /function -COPY target/*.jar /function/app/ -CMD ["com.fnproject.fn.integration.ExerciseEverything::handleRequest"] diff --git a/integration-tests/main/test-4/README.md b/integration-tests/main/test-4/README.md deleted file mode 100644 index b5d34e12..00000000 --- a/integration-tests/main/test-4/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Using this to test - -You should be able to see a trace of the various calls -in the function response. - -## To build the dependencies: - -- package up the api and runtime - -- build a docker image of the runtime - -## To test: - - fn build - fn call --display-call-id myapp /test - -or: - - echo 1, 3 | fn call --display-call-id myapp /test diff --git a/integration-tests/main/test-4/config b/integration-tests/main/test-4/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-4/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-4/expected.sh b/integration-tests/main/test-4/expected.sh deleted file mode 100755 index 1c081e44..00000000 --- a/integration-tests/main/test-4/expected.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -tail actual | grep 'Everything worked' diff --git a/integration-tests/main/test-4/func.yaml b/integration-tests/main/test-4/func.yaml deleted file mode 100644 index 376d0467..00000000 --- a/integration-tests/main/test-4/func.yaml +++ /dev/null @@ -1,8 +0,0 @@ -version: 0.0.13 -runtime: java -cmd: com.fnproject.fn.integration.ExerciseEverything::handleRequest -build: -- mvn package dependency:copy-dependencies -DincludeScope=runtime -DskipTests=true - -Dmdep.prependGroupId=true -DoutputDirectory=target -format: http -timeout: 120 diff --git a/integration-tests/main/test-4/input b/integration-tests/main/test-4/input deleted file mode 100644 index 8b137891..00000000 --- a/integration-tests/main/test-4/input +++ /dev/null @@ -1 +0,0 @@ - diff --git a/integration-tests/main/test-4/pom.xml b/integration-tests/main/test-4/pom.xml deleted file mode 100644 index 380c0d33..00000000 --- a/integration-tests/main/test-4/pom.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - 4.0.0 - - UTF-8 - 1.0.0-SNAPSHOT - - com.fnproject.fn - integration-test-4 - 1.0.0 - - jar - - - - com.fnproject.fn - api - ${fnproject.version} - - - - com.fnproject.fn - runtime - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test - - - commons-io - commons-io - 2.5 - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - - diff --git a/integration-tests/main/test-4/pre-test.sh b/integration-tests/main/test-4/pre-test.sh deleted file mode 100755 index f6e539dc..00000000 --- a/integration-tests/main/test-4/pre-test.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -ex - -cp Dockerfile.custom Dockerfile diff --git a/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java b/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java deleted file mode 100644 index b11c1a20..00000000 --- a/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/ExerciseEverything.java +++ /dev/null @@ -1,557 +0,0 @@ -package com.fnproject.fn.integration; - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.flow.*; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.TeeOutputStream; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.PrintStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.*; -import java.util.stream.Collectors; - -@SuppressWarnings("unused") -public class ExerciseEverything { - - private boolean okay = true; - private ByteArrayOutputStream bos = new ByteArrayOutputStream(); - private PrintStream out = new PrintStream(new TeeOutputStream(System.err, bos)); - private String testSelector = null; - private InputEvent inputEvent; - private List failures = new ArrayList<>(); - - @Test(1) - @Test.Expect("completed value") - public FlowFuture completedValue(Flow fl) { - return fl.completedValue("completed value"); - } - - @Test(2) - @Test.Expect("supply") - public FlowFuture supply(Flow fl) { - return fl.supply(() -> "supply"); - } - - @Test(3) - public FlowFuture allOfWithCompletedValue(Flow fl) { - return fl.allOf( - fl.completedValue(1), - fl.completedValue(2), - fl.completedValue(3) - ); - } - - @Test(4) - public FlowFuture allOfWithSuppliedValue(Flow fl) { - return fl.allOf( - fl.supply(() -> 1), - fl.supply(() -> 2), - fl.supply(() -> 3) - ); - } - - @Test(5) - @Test.Expect("1") - @Test.Expect("2") - @Test.Expect("3") - public FlowFuture anyOfWithCompletedValue(Flow fl) { - return fl.anyOf( - fl.completedValue("1"), - fl.completedValue("2"), - fl.completedValue("3") - ); - } - - @Test(6) - @Test.Expect("1") - @Test.Expect("2") - @Test.Expect("3") - public FlowFuture anyOfWithSuppliedValue(Flow fl) { - return fl.anyOf( - fl.supply(() -> "1"), - fl.supply(() -> "2"), - fl.supply(() -> "3") - ); - } - - @Test(7) - @Test.Expect("test exception") - public FlowFuture completeWithAnException(Flow fl) { - return fl.completedValue(new Exception("test exception")); - } - - @Test(8) - @Test.Catch({FlowCompletionException.class, MyException.class}) - public FlowFuture supplyAnException(Flow fl) { - return fl.supply(() -> { - throw new MyException("test exception"); - }); - } - - public static class MyException extends RuntimeException { - MyException(String m) { - super(m); - } - } - - @Test(9) - @Test.Expect("4") - public FlowFuture chainThenApply(Flow fl) { - FlowFuture cf = fl.completedValue(0); - - for (int i = 0; i < 4; i++) { - cf = cf.thenApply((x) -> x + 1); - } - return cf; - } - - - @Test(10) - @Test.Expect("-3") - public FlowFuture catchBubbledException(Flow fl) { - return fl.completedValue(0) - .thenApply((x) -> x + 1) - .thenApply((x) -> { - if (x == 1) throw new MyException("boom"); - else return x + 1; - }) - .thenApply((x) -> x + 1) - .thenApply((x) -> x + 1) - .exceptionally((e) -> -3); - } - - @Test(11) - @Test.Catch({FlowCompletionException.class, FunctionInvocationException.class}) - public FlowFuture nonexistentExternalEvaluation(Flow fl) { - return fl.invokeFunction("nonexistent/nonexistent", HttpMethod.POST, Headers.emptyHeaders(), new byte[0]); - } - - @Test(12) - @Test.Expect("okay") - public FlowFuture checkPassingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "PASS".getBytes()) - .thenApply((resp) -> resp.getStatusCode() != 200 ? "failure" : new String(resp.getBodyAsBytes())); - } - - // There is currently no way for a hot function to signal failure in the Fn platform. - // This test will only work in default mode. - @Test(13) - @Test.Catch({FlowCompletionException.class, FunctionInvocationException.class}) - public FlowFuture checkFailingExternalInvocation(Flow fl) { - return fl.invokeFunction(inputEvent.getAppName() + inputEvent.getRoute(), HttpMethod.POST, Headers.emptyHeaders(), "FAIL".getBytes()); - } - - @Test(14) - @Test.Expect("X") - public FlowFuture simpleThenCompose(Flow fl) { - return fl.completedValue("x").thenCompose((s) -> { - System.err.println("I am in the thenCompose stage, s = " + s); - FlowFuture retVal = fl.completedValue(s.toUpperCase()); - System.err.println("my retVal = " + retVal + "; type is " + retVal.getClass()); - return retVal; - }); - } - - @Test(15) - @Test.Expect("hello world") - public FlowFuture thenCompose(Flow fl) { - return fl.completedValue("hello") - .thenCompose((s) -> - fl.supply(() -> s) - .thenApply((s2) -> s2 + " world") - ); - } - - @Test(16) - @Test.Expect("foo") - public FlowFuture thenComposeThenError(Flow fl) { - return fl.completedValue("hello") - .thenCompose((s) -> fl.supply(() -> { - if (s.equals("hello")) throw new MyException("foo"); - else return s; - })) - .exceptionally(Throwable::getMessage); - } - - @Test(17) - @Test.Expect("foo") - public FlowFuture thenComposeWithErrorInBody(Flow fl) { - return fl.completedValue("hello") - .thenCompose((s) -> { - if (s.equals("hello")) throw new MyException("foo"); - else return fl.completedValue(s); - }) - .exceptionally(Throwable::getMessage); - } - - @Test(18) - @Test.Expect("a") - @Test.Expect("b") - public FlowFuture applyToEither(Flow fl) { - return fl.completedValue("a").applyToEither(fl.completedValue("b"), (x) -> x); - } - - @Test(19) - @Test.Expect("a") - @Test.Expect("b") - public FlowFuture applyToEitherLikelyPathB(Flow fl) { - return fl.supply(() -> "a").applyToEither(fl.completedValue("b"), (x) -> x); - } - - @Test(20) - public FlowFuture harmlessAcceptBoth(Flow fl) { - return fl.completedValue("a") - .thenAcceptBoth( - fl.completedValue("b"), - (a, b) -> System.err.println(a + "; " + b) - ); - } - - @Test(21) - @Test.Catch({FlowCompletionException.class, MyException.class}) - @Test.Expect("ab") - public FlowFuture acceptBoth(Flow fl) { - return fl.completedValue("a") - .thenAcceptBoth( - fl.completedValue("b"), - (a, b) -> { - System.err.println("A is " + a + " and B is " + b); - throw new MyException(a + b); - }); - } - - @Test(22) - @Test.Catch({FlowCompletionException.class, MyException.class}) - @Test.Expect("a") - @Test.Expect("b") - public FlowFuture acceptEither(Flow fl) { - return fl.completedValue("a") - .acceptEither( - fl.completedValue("b"), - (x) -> { - throw new MyException(x); - } - ); - } - - @Test(23) - @Test.Expect("foobar") - public FlowFuture thenCombine(Flow fl) { - return fl.completedValue("foo") - .thenCombine(fl.completedValue("bar"), - (a, b) -> a + b); - } - - @Test(24) - @Test.Expect("foo") - public FlowFuture thenCombineE1(Flow fl) { - return fl.supply(() -> { - throw new MyException("foo"); - }) - .thenCombine(fl.completedValue("bar"), - (a, b) -> a + b) - .exceptionally(Throwable::getMessage); - } - - @Test(25) - @Test.Expect("bar") - public FlowFuture thenCombineE2(Flow fl) { - return fl.completedValue("foo") - .thenCombine(fl.supply(() -> { - throw new MyException("bar"); - }), - (a, b) -> a + b) - .exceptionally(Throwable::getMessage); - } - - - @Test(26) - @Test.Expect("foobar") - public FlowFuture thenCombineE3(Flow fl) { - return fl.completedValue("foo") - .thenCombine(fl.completedValue("bar"), - (a, b) -> { - if (!a.equals(b)) throw new MyException(a + b); - else return "baz"; - }) - .exceptionally(Throwable::getMessage); - } - - @Test(27) - @Test.Expect("foo") - public FlowFuture handleNoError(Flow fl) { - return fl.completedValue("foo") - .handle((v, e) -> v); - } - - @Test(28) - @Test.Expect("bar") - public FlowFuture handleWithError(Flow fl) { - return fl.supply(() -> { - throw new MyException("bar"); - }) - .handle((v, e) -> e.getMessage()); - } - - @Test(29) - @Test.Expect("foo") - public FlowFuture whenCompleteNoError(Flow fl) { - return fl.completedValue("foo") - .whenComplete((v, e) -> { - System.err.println("In whenComplete, v=" + v); - throw new MyException(v); - }) - .exceptionally(t -> { - // Should *not* get called. - System.err.println("In whenComplete.exceptionally, t=" + t); - return t.getMessage() + "bar"; - }); - } - - @Test(30) - @Test.Expect("barbaz") - public FlowFuture whenCompleteWithError(Flow fl) { - return fl.supply(() -> { - if (true) throw new MyException("bar"); - else return ""; - }).whenComplete((v, e) -> { - throw new MyException(e.getMessage()); - }).exceptionally(t -> t.getMessage() + "baz"); - } - - @Test(35) - @Test.Expect("foobar") - public FlowFuture exceptionallyComposeHandle(Flow fl) throws IOException { - - return fl.failedFuture(new RuntimeException("foobar")) - .exceptionallyCompose((e) -> fl.completedValue(e.getMessage())); - } - - @Test(36) - @Test.Expect("foobar") - public FlowFuture exceptionallyComposePassThru(Flow fl) throws IOException { - - return fl.completedValue("foobar") - .exceptionallyCompose((e) -> fl.completedValue(e.getMessage())); - } - - - @Test(37) - @Test.Expect("foobar") - public FlowFuture exceptionallyComposePropagateError(Flow fl) throws IOException { - return fl.failedFuture(new RuntimeException("foo")) - .exceptionallyCompose((e) -> { - throw new RuntimeException("foobar"); - }).exceptionally(Throwable::getMessage); - } - - @Test(38) - @Test.Catch({FlowCompletionException.class, MyException.class}) - public FlowFuture allOfWithFailedValue(Flow fl) { - return fl.allOf( - fl.supply(() -> 1), - fl.supply(() -> 2), - fl.supply(() -> { - throw new MyException("foobar"); - }) - ); - } - - - @Test(39) - @Test.Expect("foobar") - public FlowFuture completeFuture(Flow fl) { - FlowFuture a = fl.createFlowFuture(); - FlowFuture b = fl.createFlowFuture(); - - a.complete("foobar"); - - return a.applyToEither(b, (s) -> { - b.cancel(); - return s; - }); - } - - private int id; - - private void fail() { - if (!failures.contains(id)) { - failures.add(id); - } - okay = false; - } - - public String handleRequest(InputEvent ie) { - this.inputEvent = ie; - String selector = ie.consumeBody((InputStream is) -> { - try { - return IOUtils.toString(is, "utf-8"); - } catch (IOException e) { - return "FAIL"; - } - }); - - if ("PASS".equals(selector)) { - return "okay"; - } else if ("FAIL".equals(selector)) { - throw new MyException("failure demanded"); - } - testSelector = selector; - Flow fl = Flows.currentFlow(); - - out.println("In main function"); - Map> awaiting = new TreeMap<>(); - - for (Map.Entry e : findTests(this).entrySet()) { - id = e.getKey(); - Method m = e.getValue(); - - Test.Catch exWanted = m.getAnnotation(Test.Catch.class); - String[] values = expectedValues(m); - - try { - awaiting.put(id, (FlowFuture) m.invoke(this, fl)); - } catch (InvocationTargetException ex) { - out.println("Failure setting up test " + id + ": " + ex.getCause()); - ex.printStackTrace(out); - fail(); - } catch (IllegalAccessException ignored) { - } - } - - for (Map.Entry e : findTests(this).entrySet()) { - id = e.getKey(); - Method m = e.getValue(); - - out.println("Test " + id + ": Start"); - - Test.Catch exWanted = m.getAnnotation(Test.Catch.class); - String[] values = expectedValues(m); - try { - FlowFuture cf = awaiting.get(id); - if (cf == null) { - continue; - } - Object r = cf.get(); - - // Coerce returned value to string - String rv = coerceToString(r); - - if (!huntForValues(rv, values)) { - fail(); - } - - if (exWanted != null) { - out.println("Test " + id + ": Failed: expecting throw of " + Arrays.toString(exWanted.value())); - fail(); - } - } catch (Throwable t) { - if (exWanted != null) { - // We have a series of wrapped exceptions that should follow this containment pattern - boolean found = false; - - for (Class c : exWanted.value()) { - if (t == null) { - out.println("Test" + id + ": Failed: end of exception chain, wanted " + c); - fail(); - break; - } - if (c.isAssignableFrom(t.getClass())) { - String message = coerceToString(t); - found = found || huntForValues(message, values); - } else { - out.println("Test" + id + ": Failed: exception type mismatch: " + t + ", wanted " + c); - t.printStackTrace(out); - fail(); - break; - } - t = t.getCause(); - } - if (!found && values.length > 0) { - out.println("Test" + id + ": Failed: failed comparison, wanted exception with one of " + Arrays.toString(values)); - fail(); - } - } else { - out.println("Test" + id + ": Failed: got an unexpected exception: " + t); - t.printStackTrace(out); - fail(); - } - } - if (!failures.contains(id)) { - out.println("Test " + id + ": Passed"); - } - } - - out.println(okay ? "Everything worked" : "There were failures: " + failures); - out.flush(); - return bos.toString(); - } - - private String coerceToString(Object r) { - if (r == null) { - return null; - } else if (r instanceof String) { - // okay - return (String) r; - } else if (r instanceof Throwable) { - return ((Throwable) r).getMessage(); - } else if (r instanceof HttpRequest) { - return new String(((HttpRequest) r).getBodyAsBytes()); - } else if (r instanceof HttpResponse) { - return new String(((HttpResponse) r).getBodyAsBytes()); - } else { - return r.toString(); - } - } - - private String[] expectedValues(Method m) { - Test.Expected ex = m.getAnnotation(Test.Expected.class); - if (ex != null) { - return Arrays.stream(ex.value()).map(Test.Expect::value).toArray(String[]::new); - } else { - Test.Expect ex2 = m.getAnnotation(Test.Expect.class); - if (ex2 != null) { - return new String[]{ex2.value()}; - } else { - return new String[]{}; - } - } - } - - private boolean huntForValues(String match, String... values) { - for (String v : values) { - if ((v == null && match == null) || (v != null && v.equals(match))) { - return true; - } - } - if (values.length > 0) { - return false; - } - return true; - } - - private Map findTests(Object target) { - Map tests = new TreeMap<>(); - for (Method m : target.getClass().getMethods()) { - Test ann = m.getAnnotation(Test.class); - if (ann == null) - continue; - int id = ann.value(); - tests.put(id, m); - } - if (testSelector == null || testSelector.trim().isEmpty()) { - return tests; - } - - return Arrays.stream(testSelector.split(",")) - .map(String::trim) - .map(Integer::valueOf) - .filter(tests::containsKey) - .collect(Collectors.toMap((x) -> x, tests::get)); - } -} diff --git a/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/Test.java b/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/Test.java deleted file mode 100644 index 46380a7c..00000000 --- a/integration-tests/main/test-4/src/main/java/com/fnproject/fn/integration/Test.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.fnproject.fn.integration; - -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -@Retention(RetentionPolicy.RUNTIME) -public @interface Test { - int value(); - - @Retention(RetentionPolicy.RUNTIME) - @interface Expected { - Expect[] value(); - } - - @Retention(RetentionPolicy.RUNTIME) - @Repeatable(Expected.class) - @interface Expect { - String value(); - } - - @Retention(RetentionPolicy.RUNTIME) - @interface Catch { - Class[] value(); - } -} diff --git a/integration-tests/main/test-5/config b/integration-tests/main/test-5/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-5/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-5/expected.sh b/integration-tests/main/test-5/expected.sh deleted file mode 100755 index fd53afcd..00000000 --- a/integration-tests/main/test-5/expected.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# Horrible bash checker... - -FOUND_FILENAME=`pwd`/success -rm -f "$FOUND_FILENAME" -ATTEMPT=0 -while [ ! -f "$FOUND_FILENAME" ] ; -do - sleep 1 - calls_found=`fn calls list "test-5" | grep "Status: success" | wc -l` - echo "$calls_found successful function calls found" - - # TODO: Remove this check when `fn logs` becomes reliable - if [[ -n `echo $calls_found | grep "3"` ]]; then - touch "$FOUND_FILENAME" - fi - - # TODO: Use this check instead when `fn logs` becomes reliable - # fn calls list "test-5" | while read k v - # do - # if [[ "$k" = "ID:" ]]; then id="$v"; fi - # if [[ -z "$k" ]]; then - # LOG=`fn logs get "test-5" "$id"` - # echo $LOG - # if [[ $LOG == *"Ran the hook."* ]]; then - # touch "$FOUND_FILENAME" - # fi - # fi - # done - - ATTEMPT=$((ATTEMPT + 1)) - if [ $ATTEMPT -ge 120 ]; - then - # echo "Did not find termination hook output" - echo "Termination hook was not called or failed" - exit 1 - fi -done diff --git a/integration-tests/main/test-5/func.yaml b/integration-tests/main/test-5/func.yaml deleted file mode 100644 index a5886022..00000000 --- a/integration-tests/main/test-5/func.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 0.0.1 -runtime: java9 -cmd: com.fnproject.fn.integration.test_5.CompleterFunction::handleRequest -format: http -timeout: 120 diff --git a/integration-tests/main/test-5/input b/integration-tests/main/test-5/input deleted file mode 100644 index e69de29b..00000000 diff --git a/integration-tests/main/test-5/pom.xml b/integration-tests/main/test-5/pom.xml deleted file mode 100644 index 400510fc..00000000 --- a/integration-tests/main/test-5/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 4.0.0 - - UTF-8 - 1.0.0-SNAPSHOT - - com.fnproject.fn - integration-test-5 - 1.0.0 - - jar - - - - com.fnproject.fn - api - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - - diff --git a/integration-tests/main/test-5/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java b/integration-tests/main/test-5/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java deleted file mode 100644 index e4d2e3c7..00000000 --- a/integration-tests/main/test-5/src/main/java/com/fnproject/fn/integration/test_5/CompleterFunction.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.fnproject.fn.integration.test_5; - -import com.fnproject.fn.api.flow.Flow; -import com.fnproject.fn.api.flow.Flows; - -public class CompleterFunction { - - public Integer handleRequest(String input) { - Flow fl = Flows.currentFlow(); - fl.addTerminationHook( (ignored) -> { System.err.println("Ran the hook."); }); - return fl.supply(() -> { Thread.sleep(1000); return 42; }).get(); - } - -} diff --git a/integration-tests/main/test-6/config b/integration-tests/main/test-6/config deleted file mode 100644 index a7c1a5a8..00000000 --- a/integration-tests/main/test-6/config +++ /dev/null @@ -1 +0,0 @@ -COMPLETER_BASE_URL=${COMPLETER_BASE_URL} diff --git a/integration-tests/main/test-6/expected.sh b/integration-tests/main/test-6/expected.sh deleted file mode 100755 index a7bc7275..00000000 --- a/integration-tests/main/test-6/expected.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash - -# Horrible bash checker... - -FOUND_FILENAME=`pwd`/success -rm -f "$FOUND_FILENAME" -ATTEMPT=0 -while [ ! -f "$FOUND_FILENAME" ] ; -do - sleep 1 - calls_found=`fn calls list "test-6" | grep "Status: success" | wc -l` - echo "$calls_found successful function calls found" - - fn calls list "test-6" | while read k v - do - if [[ "$k" = "ID:" ]]; then id="$v"; fi - if [[ -z "$k" ]]; then - LOG=`fn logs get "test-6" "$id"` - echo $LOG - if [[ $LOG == *"Caught timeout"* ]]; then - touch "$FOUND_FILENAME" - fi - fi - done - - ATTEMPT=$((ATTEMPT + 1)) - if [ $ATTEMPT -ge 120 ]; - then - echo "Did not find expected output" - exit 1 - fi -done diff --git a/integration-tests/main/test-6/func.yaml b/integration-tests/main/test-6/func.yaml deleted file mode 100644 index a0cf97fe..00000000 --- a/integration-tests/main/test-6/func.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 0.0.1 -runtime: java9 -cmd: com.fnproject.fn.integration.test_6.CompleterFunction::handleRequest -format: http -timeout: 120 diff --git a/integration-tests/main/test-6/input b/integration-tests/main/test-6/input deleted file mode 100644 index e69de29b..00000000 diff --git a/integration-tests/main/test-6/pom.xml b/integration-tests/main/test-6/pom.xml deleted file mode 100644 index bacc9706..00000000 --- a/integration-tests/main/test-6/pom.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - 4.0.0 - - UTF-8 - 1.0.0-SNAPSHOT - - com.fnproject.fn - integration-test-6 - 1.0.0 - - jar - - - - com.fnproject.fn - api - ${fnproject.version} - - - com.fnproject.fn - testing - ${fnproject.version} - test - - - junit - junit - 4.12 - test - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.8 - 1.8 - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - - - fn-maven-releases - https://dl.bintray.com/fnproject/fnproject - - - diff --git a/integration-tests/main/test-6/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java b/integration-tests/main/test-6/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java deleted file mode 100644 index 5041c8dc..00000000 --- a/integration-tests/main/test-6/src/main/java/com/fnproject/fn/integration/test_6/CompleterFunction.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.fnproject.fn.integration.test_6; - -import com.fnproject.fn.api.flow.Flow; -import com.fnproject.fn.api.flow.Flows; - -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -public class CompleterFunction { - - public Integer handleRequest(String input) { - Flow fl = Flows.currentFlow(); - try { - return fl.supply(() -> { - Thread.sleep(10000); - return 42; - }).get(1000, TimeUnit.MILLISECONDS); - } catch (TimeoutException t) { - System.err.println("Caught timeout"); - return 20; - } - } - -} diff --git a/integration-tests/main/test-7/delete.sh b/integration-tests/main/test-7/delete.sh deleted file mode 100644 index 75a939ed..00000000 --- a/integration-tests/main/test-7/delete.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -ex - -fn routes delete "$TESTNAME" /test-7 diff --git a/integration-tests/main/test-7/deploy.sh b/integration-tests/main/test-7/deploy.sh deleted file mode 100755 index 515e2071..00000000 --- a/integration-tests/main/test-7/deploy.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -ex - -fn deploy --app "$TESTNAME" --local diff --git a/integration-tests/main/test-7/expected b/integration-tests/main/test-7/expected deleted file mode 100644 index 5dd01c17..00000000 --- a/integration-tests/main/test-7/expected +++ /dev/null @@ -1 +0,0 @@ -Hello, world! \ No newline at end of file diff --git a/integration-tests/main/test-7/pre-test.sh b/integration-tests/main/test-7/pre-test.sh deleted file mode 100755 index f4699600..00000000 --- a/integration-tests/main/test-7/pre-test.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -ex - -set -ex - -rm -rf Dockerfile func.yaml pom.xml src -FN_JAVA_FDK_VERSION=$(cat ../../../release.version) fn init --runtime=java --name app/hello diff --git a/integration-tests/main/test-7/run-test.sh b/integration-tests/main/test-7/run-test.sh deleted file mode 100755 index 0af4edc3..00000000 --- a/integration-tests/main/test-7/run-test.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -ex - -fn call "$TESTNAME" /test-7 > actual diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml deleted file mode 100644 index 30588095..00000000 --- a/integration-tests/pom.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - com.fnproject.fn - fdk - 1.0.0-SNAPSHOT - - - 4.0.0 - - com.fnproject.flow - integration-tests - - pom - - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.8.2 - - true - - - - - - diff --git a/integration-tests/post-configure-hook.sh b/integration-tests/post-configure-hook.sh deleted file mode 100755 index a81dbd98..00000000 --- a/integration-tests/post-configure-hook.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -# This hook runs in the test directory immediately prior to any "fn build" invocation. - -# Turn these lines in func.yaml: - -# name: jbloggs/fn-flows-function -# version: 0.0.1 - -# into these: - -# name: docker-registry:5000/jbloggs/fn-flows-function -# version: 4837492387439724389 <- whatever suffix is. - -set -ex - -docker push $(awk '/^name:/ { print $2 }' func.yaml):$SUFFIX -mv .func.yaml-old func.yaml diff --git a/integration-tests/pre-build-hook.sh b/integration-tests/pre-build-hook.sh deleted file mode 100755 index 1eb1d178..00000000 --- a/integration-tests/pre-build-hook.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -# This hook runs in the test directory immediately prior to any "fn build" invocation. - -# Turn these lines in func.yaml: - -# name: jbloggs/fn-flow-function -# version: 0.0.1 - -# into these: - -# name: docker-registry:5000/jbloggs/fn-flow-function -# version: 4837492387439724389 <- whatever suffix is. - -set -e - -while read key rest -do - case "$key" in - name:) - rest="docker-registry:5000/$rest" - ;; - version:) - rest="$SUFFIX" - ;; - esac - echo "$key $rest" -done < func.yaml > .func.yaml-new - -mv func.yaml .func.yaml-old -mv .func.yaml-new func.yaml diff --git a/integration-tests/run-all-tests.sh b/integration-tests/run-all-tests.sh deleted file mode 100755 index 68a54511..00000000 --- a/integration-tests/run-all-tests.sh +++ /dev/null @@ -1,100 +0,0 @@ -#!/bin/bash - -# Run all smoke-tests in parallel, recording their output. -# Report the results on any failures. - -source "$SCRIPT_DIR/lib.sh" - -set -ex - -# ---------------------------------------------------------------------- -# The following variables need to be set -# ---------------------------------------------------------------------- - -# This is an awful bashism -if [[ -z "${FN_API_URL+x}" ]]; then echo "Please set FN_API_URL"; exit 1; fi -if [[ -z "${COMPLETER_BASE_URL+x}" ]]; then echo "Please set COMPLETER_BASE_URL"; exit 1; fi - - -# ---------------------------------------------------------------------- -# Run each smoke-test in parallel -# ---------------------------------------------------------------------- - -printenv -fn apps list - -set +x - -SMOKE_HARNESS="$SCRIPT_DIR/smoke-test.sh" -export LIBFUNS="$SCRIPT_DIR/lib.sh" -export FN_TOKEN -export no_proxy=$no_proxy,127.0.0.1,10.167.103.241 - -cd "$SCRIPT_DIR" - -if [[ $# = 0 ]]; then - tests=main/test-* - show= - background='> "$d/output" 2>&1 &' -else - tests=$(find "$@" -type d -name test-\* -prune) - show='set -x' - background= -fi - -echo "Running tests: $tests" - -eval "$show" -for d in $tests -do - - rm -f "$d"/actual "$d"/output - eval "( - # Run the integration test - - cd \"$d\" && \"$SMOKE_HARNESS\" - ) $background" - -done -wait -set +x - - -# ---------------------------------------------------------------------- -# Report on results sequentially -# ---------------------------------------------------------------------- - -okay=1 -report() { - echo "Test $(basename "$1") expected -" - cat "$1/expected" - echo "Test $(basename "$1") actual -" - cat "$1/actual" - echo "Test $(basename "$1") output -" - cat "$1/output" - line -} - -for d in $tests -do - set +e - - if [[ -f "$d/failure" ]]; then - okay=0 - line - echo "Test $(basename "$d") failed:" - report "$d" - elif [[ -f "$d/success" ]]; then - line - echo "Test $(basename "$d") succeeded" - line - else - okay=0 - line - echo "**************** Test $(basename "$d") unknown status" - report "$d" - fi -done - -[[ $okay = 1 ]] || exit 1 -echo Success! diff --git a/integration-tests/run-local.sh b/integration-tests/run-local.sh deleted file mode 100755 index 06d01bf0..00000000 --- a/integration-tests/run-local.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bash - -# Set up a local test environment in order to run integration tests, -# then execute them. - -source "$(dirname "$0")/lib.sh" - -set -ex - -# ---------------------------------------------------------------------- -# The following variables may be set to parameterise the operation of this script -# ---------------------------------------------------------------------- - -: ${FUNCTIONS_DOCKER_IMAGE:=fnproject/fnserver} -: ${SUFFIX:=$(git rev-parse HEAD)} -: ${COMPLETER_DOCKER_IMAGE:=fnproject/flow} - -# ---------------------------------------------------------------------- -# Stand up a local staging maven directory, if needed -# ---------------------------------------------------------------------- - -if [[ -n "$REPOSITORY_LOCATION" ]]; then - REPO_CONTAINER_ID=$( - docker run -d \ - -v "$REPOSITORY_LOCATION":/repo:ro \ - -w /repo \ - --name repo-$SUFFIX \ - python:2.7 \ - python -mSimpleHTTPServer 18080 - ) - defer docker rm -f $REPO_CONTAINER_ID - REPO_INTERNAL_IP=$( - docker inspect \ - --type container \ - -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ - $REPO_CONTAINER_ID - ) - export MAVEN_REPOSITORY_LOCATION="http://$REPO_INTERNAL_IP:18080" - export no_proxy="$no_proxy,$REPO_INTERNAL_IP" -fi - -# ---------------------------------------------------------------------- -# Stand up the functions platform -# ---------------------------------------------------------------------- - -docker pull $FUNCTIONS_DOCKER_IMAGE - -FUNCTIONS_CONTAINER_ID=$( - docker run -d \ - -p 8080 \ - -v /var/run/docker.sock:/var/run/docker.sock \ - --name functions-$SUFFIX \ - -e FN_LOG_LEVEL=debug \ - $FUNCTIONS_DOCKER_IMAGE - ) -defer docker rm -f $FUNCTIONS_CONTAINER_ID -defer docker logs functions-$SUFFIX -defer echo ---- FUNCTIONS OUTPUT FOR TEST ----------------------------------------------------------- - -FUNCTIONS_HOST=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8080/tcp"}}{{.HostIp}}{{end}}' \ - $FUNCTIONS_CONTAINER_ID - ) - -FUNCTIONS_PORT=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8080/tcp"}}{{.HostPort}}{{end}}' \ - $FUNCTIONS_CONTAINER_ID - ) - -FUNCTIONS_INTERNAL_IP=$( - docker inspect \ - --type container \ - -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ - $FUNCTIONS_CONTAINER_ID - ) - -export FN_API_URL="http://$FUNCTIONS_HOST:$FUNCTIONS_PORT" -export no_proxy="$no_proxy,$FUNCTIONS_HOST" - - -# ---------------------------------------------------------------------- -# Stand up the completer -# ---------------------------------------------------------------------- - -COMPLETER_CONTAINER_ID=$( - docker run -d \ - -p 8081 \ - --env API_URL=http://${FUNCTIONS_INTERNAL_IP}:8080 \ - --env no_proxy=$no_proxy,${FUNCTIONS_INTERNAL_IP} \ - --name flow-server-$SUFFIX \ - $COMPLETER_DOCKER_IMAGE - ) -defer docker rm -f $COMPLETER_CONTAINER_ID -defer docker logs $COMPLETER_CONTAINER_ID -defer echo ---- COMPLETER OUTPUT FOR TEST ----------------------------------------------------------- - -COMPLETER_HOST=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8081/tcp"}}{{.HostIp}}{{end}}' \ - $COMPLETER_CONTAINER_ID - ) - -COMPLETER_PORT=$( - docker inspect \ - --type container \ - -f '{{range index .NetworkSettings.Ports "8081/tcp"}}{{.HostPort}}{{end}}' \ - $COMPLETER_CONTAINER_ID - ) - -COMPLETER_INTERNAL_IP=$( - docker inspect \ - --type container \ - -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \ - $COMPLETER_CONTAINER_ID - ) - -export COMPLETER_BASE_URL=http://$COMPLETER_INTERNAL_IP:8081 -export no_proxy="$no_proxy,$COMPLETER_HOST" - - -# ---------------------------------------------------------------------- -# Wait for the containers to become ready -# ---------------------------------------------------------------------- - -export HTTP_PROXY="$http_proxy" -export HTTPS_PROXY="$https_proxy" -export NO_PROXY="$no_proxy" - -wait_for_http "$FN_API_URL" -wait_for_http "http://$COMPLETER_HOST:$COMPLETER_PORT/ping" - -set +x - -"$SCRIPT_DIR/run-all-tests.sh" "$@" diff --git a/integration-tests/run-remote.sh b/integration-tests/run-remote.sh deleted file mode 100755 index 69a1ac4f..00000000 --- a/integration-tests/run-remote.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -# Set up a local test environment in order to run integration tests, -# then execute them. - -source "$(dirname "$0")/lib.sh" - -set -ex - -# ---------------------------------------------------------------------- -# The following variables may be set to parameterise the operation of this script -# ---------------------------------------------------------------------- - -export SUFFIX=$(git rev-parse HEAD) -export FN_TOKEN=${FN_TOKEN:-$(cat ~/.fn-token)} - - -# ---------------------------------------------------------------------- -# The following variables should be set in the integration environment -# ---------------------------------------------------------------------- - -export FN_API_URL=$(cat ~/.fn-api-url) -export COMPLETER_BASE_URL=$(cat ~/.fn-flow-base-url) - - -# We need to push our images into the test environment, so let's ensure that our tunnel is set up -systemctl --user restart ssh-tunnels - -# Ensure we have the hooks we want in place -export PRE_BUILD_HOOK="$SCRIPT_DIR/pre-build-hook.sh" -export POST_CONFIGURE_HOOK="$SCRIPT_DIR/post-configure-hook.sh" - -export HTTP_PROXY="$http_proxy" -export HTTPS_PROXY="$https_proxy" -export NO_PROXY="$no_proxy" - -set +x - -"$SCRIPT_DIR/run-all-tests.sh" "$@" diff --git a/integration-tests/smoke-test.sh b/integration-tests/smoke-test.sh deleted file mode 100755 index 324b895a..00000000 --- a/integration-tests/smoke-test.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/bin/bash - -. "$LIBFUNS" - -# Run an individual smoketest - -# Environmental requirements: -# - cwd is the directory of the smoketest to run -# - LIBFUNS points to the shell helper library -# - up-to-date "fn" command on the PATH -# - FN_TOKEN is set to something that the functions platform will approve -# - FN_API_URL points to the functions platform endpoint -# - any http_proxy, etc. settings are correct to permit access to that endpoint and any maven repos required by fn build -# - COMPLETER_BASE_URL is set to a value that should be configured on the target function -# - MAVEN_REPOSITORY_LOCATION, if set, corresponds to the URL that should be replaced in the test pom files. -# - the runtime docker image is up-to-date - -rm -f success failure Dockerfile -export TESTNAME="$(basename $(pwd))" -set -ex - -if [ -f pre-test.sh ]; then - ./pre-test.sh -fi - -# Replace the maven repo with a staging location, if required -if [ -n "$MAVEN_REPOSITORY_LOCATION" ]; then - sed -i.bak \ - -e "s|https://dl.bintray.com/fnproject/fnproject|$MAVEN_REPOSITORY_LOCATION|g" \ - pom.xml - rm pom.xml.bak -fi - -# Build the integration test - -[[ -n "$PRE_BUILD_HOOK" ]] && $PRE_BUILD_HOOK - -fn build --no-cache >build-output 2>&1 || { - echo "Test function build failed:" - cat build-output - exit 1 -} - -if [ -f config ]; then - fn apps create "$TESTNAME" $(echo $(prefix_lines --config config)) -else - fn apps create "$TESTNAME" -fi - -if [[ -x deploy.sh ]] -then - ./deploy.sh -else - fn deploy --app "$TESTNAME" --local -fi - -[[ -n "$POST_CONFIGURE_HOOK" ]] && $POST_CONFIGURE_HOOK - -fn apps inspect "$TESTNAME" -[[ -x route-create.sh ]] || fn routes inspect "$TESTNAME" "$TESTNAME" - -if [[ -x run-test.sh ]] -then - ./run-test.sh -else - curl -v "$FN_API_URL/r/$TESTNAME/$TESTNAME" -d @input > actual -fi - -if [[ -x expected.sh ]] -then - ./expected.sh && touch success || touch failure -else - diff --ignore-all-space -u expected actual && touch success || touch failure -fi - -set +x -fn calls list "$TESTNAME" | while read k v -do - echo "$k $v" - if [[ "$k" = "ID:" ]]; then id="$v"; fi - if [[ -z "$k" ]]; then - echo '[[[' - fn logs get "$TESTNAME" "$id" - echo ']]]' - echo - fi -done - -set -x - -if [[ -x delete.sh ]] -then - ./delete.sh -fi -fn apps delete "$TESTNAME" diff --git a/pom.xml b/pom.xml index 837a23bc..283110c0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,42 +1,252 @@ + + + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 fdk com.fnproject.fn pom 1.0.0-SNAPSHOT + fdk-java + The Function Development Kit for Java makes it easy to build and deploy Java functions to Fn + https://fnproject.io/tutorials/JavaFDKIntroduction/ + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + fnproject/core + roneet.shaw@oracle.com + oracle + https://www.oracle.com/ + + + + + scm:git:git://github.com/fnproject/fdk-java.git + scm:git:ssh://github.com:fnproject/fdk-java.git + https://github.com/fnproject/fdk-java + + + + + central + https://central.sonatype.com/api/v1/staging/deploy/maven2 + + + central + https://central.sonatype.com/repository/maven-snapshots/ + + + api runtime - fn-spring-cloud-function + testing-core + testing-junit4 testing + flow-api + flow-runtime + flow-testing + fn-spring-cloud-function + fn-events + fn-events-testing examples - integration-tests + experimental-native-image-support UTF-8 - 0.7.9 - 1.7.25 - 2.5 - 2.8.7 - 2.8.47 - 3.6.2 - 4.4.6 + UTF-8 + 3.21.0 + 2.14.0 + 4.4.14 + 2.21.2 + 0.8.1 + 9.4.12.v20180830 + 4.13.2 + 4.0.0 + 1.4.0 + 1.7.25 + 2.22.1 1.16.0 - 4.12 + 21.2.0 + false + + + + + + com.fnproject.fn + api + ${project.version} + + + + com.fnproject.fn + flow-api + ${project.version} + + + com.fnproject.fn + flow-runtime + ${project.version} + + + com.fnproject.fn + fn-events + ${project.version} + + + com.fnproject.fn + runtime + ${project.version} + + + com.fnproject.fn + testing + ${project.version} + + + com.fnproject.fn + testing-core + ${project.version} + + + com.fnproject.fn + testing-junit4 + ${project.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + org.apache.httpcomponents + httpcore + ${httpcore.version} + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + commons-io + commons-io + ${commons-io.version} + + + commons-logging + commons-logging + 1.2 + + + net.jodah + typetools + 0.6.3 + + + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + junit + junit + ${junit.version} + test + + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + maven-dependency-plugin + 3.1.1 + + + org.netbeans.tools + sigtest-maven-plugin + 1.0 + + + org.pitest + pitest-maven + ${pitest.version} + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + + org.apache.maven.plugins maven-compiler-plugin - 3.6.1 + 3.8.0 javac-with-errorprone true @@ -47,19 +257,19 @@ org.codehaus.plexus plexus-compiler-javac-errorprone - 2.8 + 2.8.4 com.google.errorprone error_prone_core - 2.0.21 + 2.3.1 org.apache.maven.plugins maven-source-plugin - 3.0.0 + 3.0.1 attach-sources @@ -69,6 +279,31 @@ + + maven-resources-plugin + 3.1.0 + + + copy-resources + validate + + copy-resources + + + ${project.basedir}/target/classes/META-INF/ + + + ${user.dir} + + THIRD_PARTY_LICENSES.txt + LICENSE + + + + + + + org.jacoco jacoco-maven-plugin @@ -103,13 +338,71 @@ + + + org.pitest + pitest-maven + ${pitest.version} + com.spotify dockerfile-maven-extension - 1.3.1 + 1.4.3 + + + + + _qm-qs + + false + + + + + org.codehaus.mojo + versions-maven-plugin + 2.5 + + + + + + ci-cd + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + central + true + published + + + + + + + diff --git a/release.version b/release.version index ed453e69..e9bc1499 100644 --- a/release.version +++ b/release.version @@ -1 +1 @@ -1.0.56 +1.1.14 diff --git a/runtime/Dockerfile b/runtime/Dockerfile deleted file mode 100644 index cb69e29e..00000000 --- a/runtime/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM openjdk:8-slim -COPY target/runtime-*.jar target/dependency/*.jar /function/runtime/ - -RUN ["/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java", "-Xshare:dump"] - -# UseCGroupMemoryLimitForHeap looks up /sys/fs/cgroup/memory/memory.limit_in_bytes inside the container to determine -# what the heap should be set to. This is an experimental feature at the moment, thus we need to unlock to use it. -# -# MaxRAMFraction is used modify the heap size and it is used as a denominator where the numerator is phys_mem. -# It seems that this value is a uint in the JVM code, thus can only specify 1 => 100%, 2 => 50%, 3 => 33.3%, 4 => 25% -# and so on. -# -# SerialGC is used here as it's likely that we'll be running many JVMs on the same host machine and it's also likely -# that the number of JVMs will outnumber the number of available processors. -# -# The max memory value obtained with these args seem to be okay for most memory limits. The exception is when the -# memory limit is set to 128MiB, in which case maxMemory returns roughly half. -ENTRYPOINT [ "/usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=2", "-XX:+UseSerialGC", "-Xshare:on", "-cp", "/function/app/*:/function/runtime/*", "com.fnproject.fn.runtime.EntryPoint" ] diff --git a/runtime/Dockerfile-jdk9 b/runtime/Dockerfile-jdk9 deleted file mode 100644 index 886bebc6..00000000 --- a/runtime/Dockerfile-jdk9 +++ /dev/null @@ -1,18 +0,0 @@ -FROM openjdk:9-slim -COPY target/runtime-*.jar target/dependency/*.jar /function/runtime/ - -RUN ["/usr/bin/java", "-Xshare:dump"] - -# UseCGroupMemoryLimitForHeap looks up /sys/fs/cgroup/memory/memory.limit_in_bytes inside the container to determine -# what the heap should be set to. This is an experimental feature at the moment, thus we need to unlock to use it. -# -# MaxRAMFraction is used modify the heap size and it is used as a denominator where the numerator is phys_mem. -# It seems that this value is a uint in the JVM code, thus can only specify 1 => 100%, 2 => 50%, 3 => 33.3%, 4 => 25% -# and so on. -# -# SerialGC is used here as it's likely that we'll be running many JVMs on the same host machine and it's also likely -# that the number of JVMs will outnumber the number of available processors. -# -# The max memory value obtained with these args seem to be okay for most memory limits. The exception is when the -# memory limit is set to 128MiB, in which case maxMemory returns roughly half. -ENTRYPOINT [ "/usr/bin/java", "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap", "-XX:MaxRAMFraction=2", "-XX:+UseSerialGC", "-Xshare:on", "-cp", "/function/app/*:/function/runtime/*", "com.fnproject.fn.runtime.EntryPoint" ] diff --git a/runtime/pom.xml b/runtime/pom.xml index 89800faa..70de9733 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -1,4 +1,22 @@ + + @@ -8,80 +26,107 @@ 1.0.0-SNAPSHOT 4.0.0 - + runtime runtime - - UTF-8 - - - com.fnproject.fn api - ${project.version} - com.fasterxml.jackson.core jackson-databind - ${jackson.version} - org.apache.httpcomponents httpcore - ${httpcore.version} + commons-io commons-io - ${commons-io.version} net.jodah typetools - 0.5.0 org.mockito mockito-core - ${mockito.version} + test + + + junit + junit test org.assertj assertj-core - ${assertj-core.version} test - - junit - junit - 4.12 + org.apache.httpcomponents + httpclient + test + + + org.eclipse.jetty + jetty-client + ${jetty.version} test - org.apache.httpcomponents - httpmime - 4.5.3 + org.eclipse.jetty + jetty-unixsocket + ${jetty.version} test + + + + src/main/java-filtered + ${build.directory}/version-sources + true + + + src/main/c + + *.so + + false + ${build.directory}/classes/META-INF/com.fnproject.fn/runtime/native + + + src/main/c/amd64 + + *.so + + false + ${build.directory}/classes/META-INF/com.fnproject.fn/runtime/native/amd64 + + + src/main/c/arm64 + + *.so + + false + ${build.directory}/classes/META-INF/com.fnproject.fn/runtime/native/arm64 + + maven-dependency-plugin - 3.0.1 copy-dependencies @@ -97,6 +142,45 @@ + + org.codehaus.mojo + build-helper-maven-plugin + + + generate-sources + + add-source + + + + ${project.build.directory}/version-sources + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + + ${user.dir} + + + diff --git a/runtime/smith.yaml b/runtime/smith.yaml index 707b88a4..91aa5168 100644 --- a/runtime/smith.yaml +++ b/runtime/smith.yaml @@ -1,3 +1,19 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + package: https://registry-1.docker.io/library/openjdk:8-slim type: oci paths: diff --git a/runtime/src/main/c/.gitignore b/runtime/src/main/c/.gitignore new file mode 100644 index 00000000..417eb980 --- /dev/null +++ b/runtime/src/main/c/.gitignore @@ -0,0 +1,4 @@ +build/ +libfnunixsocket.so +libfnunixsocket.dylib +cmake-build-debug diff --git a/runtime/src/main/c/CMakeLists.txt b/runtime/src/main/c/CMakeLists.txt new file mode 100644 index 00000000..61d79e3d --- /dev/null +++ b/runtime/src/main/c/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.5) +project(fnunixsocket) +set(CMAKE_BUILD_TYPE Release) +set(JAVA_AWT_LIBRARY NotNeeded) +set(JAVA_JVM_LIBRARY NotNeeded) +set(JAVA_AWT_INCLUDE_PATH NotNeeded) +find_package(JNI REQUIRED) +include_directories(${JNI_INCLUDE_DIRS}) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall") + + +add_library(fnunixsocket SHARED unix_socket.c) diff --git a/runtime/src/main/c/Dockerfile-buildimage b/runtime/src/main/c/Dockerfile-buildimage new file mode 100644 index 00000000..dcfce5dc --- /dev/null +++ b/runtime/src/main/c/Dockerfile-buildimage @@ -0,0 +1,24 @@ +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +FROM container-registry.oracle.com/os/oraclelinux:9-slim +RUN microdnf install -y \ + gcc gcc-c++ cmake make java-1.8.0-openjdk-devel && \ + microdnf clean all \ +RUN mkdir /build +WORKDIR /build + + diff --git a/runtime/src/main/c/README.md b/runtime/src/main/c/README.md new file mode 100644 index 00000000..7e4efe65 --- /dev/null +++ b/runtime/src/main/c/README.md @@ -0,0 +1,19 @@ +# Native components for Fn unix socket protocol + +This is a very simple JNI binding to expose unix sockets to the Fn runtime + +## Building + +you can rebuild a linux version (for the FDK itself) of the JNI library using `./rebuild_so.sh` this runs `buildit.sh` in a suitable docker container + +For testing on a mac you can also compile locally by running `buildit.sh`, you will need at least: + +* XCode compiler toolchain +* cmake +* make +* a JDK installed (for cmake JNI) + + +Current issues: +* This is using old-style JNI array passing which is slow - it should be using native buffers +* Doesn't support non-blocking operations, specifically reads and writes which will block indefinitely \ No newline at end of file diff --git a/runtime/src/main/c/buildit.sh b/runtime/src/main/c/buildit.sh new file mode 100755 index 00000000..23445f46 --- /dev/null +++ b/runtime/src/main/c/buildit.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +set -e + +src_dir=$(pwd) +build_dir=${src_dir}/build/$(uname -s| tr '[:upper:]' '[:lower:]') +mkdir -p ${build_dir} +( + cd ${build_dir} + cmake ${src_dir} + + make +) +mv ${build_dir}/libfnunixsocket.* ${src_dir} \ No newline at end of file diff --git a/runtime/src/main/c/rebuild_so.sh b/runtime/src/main/c/rebuild_so.sh new file mode 100755 index 00000000..95eeb510 --- /dev/null +++ b/runtime/src/main/c/rebuild_so.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +mydir=$(cd "$(dirname "$0")"; pwd) +cd ${mydir} + +set -e + +docker build -t fdk_c_build -f Dockerfile-buildimage . + +docker run -v $(pwd):/build fdk_c_build ./buildit.sh + +if [ $(uname -m) == "x86_64" ] +then + mkdir amd64 + cp libfnunixsocket.so amd64 +else + mkdir arm64 + cp libfnunixsocket.so arm64 +fi diff --git a/runtime/src/main/c/unix_socket.c b/runtime/src/main/c/unix_socket.c new file mode 100644 index 00000000..18952c44 --- /dev/null +++ b/runtime/src/main/c/unix_socket.c @@ -0,0 +1,576 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef US_DEBUG +#define debuglog(...) fprintf (stderr, __VA_ARGS__) +#else +#define debuglog(...) + +#endif + +/** + * Throws com.fnproject.fn.runtime.ntv.UnixSocetException, adding strerr(errno) as the second arg if that is set + * @param jenv java env + * @param message message to send + */ +void throwIOException(JNIEnv *jenv, const char *message) { + jclass exc = (*jenv)->FindClass(jenv, + "com/fnproject/fn/runtime/ntv/UnixSocketException"); + if (exc == NULL) { // JVM exception + return; + } + jmethodID constr = (*jenv)->GetMethodID(jenv, exc, "", + "(Ljava/lang/String;Ljava/lang/String;)V"); + + if (constr == NULL) { // JVM exception + return; + } + jstring str = (*jenv)->NewStringUTF(jenv, message); + if (str == NULL) { // JVM exception + return; + } + jstring estr = (*jenv)->NewStringUTF(jenv, strerror(errno)); + if (estr == NULL) { // JVM exception + return; + } + jthrowable t = (jthrowable) (*jenv)->NewObject(jenv, exc, constr, str, estr); + if (t == NULL) { // JVM exception + return; + } + (*jenv)->Throw(jenv, t); +} + + +/** + * Throws a single-arg string exception + * @param jenv + * @param clazz class path (e.g. "java/lang/NullPointerException" + * @param message message to pass into constructor + */ +void throwSingleArgStringException(JNIEnv *jenv, const char *clazz, const char *message) { + jclass exc = (*jenv)->FindClass(jenv, clazz); + if (exc == NULL) { + return; + } + jmethodID constr = (*jenv)->GetMethodID(jenv, exc, "", + "(Ljava/lang/String;)V"); + if (constr == NULL) { // JVM exception + return; + } + jstring str = (*jenv)->NewStringUTF(jenv, message); + if (str == NULL) { // JVM OOM + return; + } + jthrowable t = (jthrowable) (*jenv)->NewObject(jenv, exc, constr, str); + if (t == NULL) { // JVM error + return; + } + (*jenv)->Throw(jenv, t); +} + +void throwIllegalArgumentException(JNIEnv *jenv, const char *message) { + throwSingleArgStringException(jenv, "java/lang/IllegalArgumentException", message); +} + + +void throwSocketTimeoutException(JNIEnv *jenv, const char *message) { + throwSingleArgStringException(jenv, "java/net/SocketTimeoutException", message); +} + + +void throwNPE(JNIEnv *jenv, const char *message) { + throwSingleArgStringException(jenv, "java/lang/NullPointerException", message); +} + + + +// public static native int createSocket(); +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_socket(JNIEnv *jenv, jclass jClass) { + errno = 0; + int rv = socket(PF_UNIX, SOCK_STREAM, 0); + + if (rv == -1) { + throwIOException(jenv, "Could not create socket"); + return -1; + } + debuglog("got result from socket %d\n",rv); + + return rv; +} + + + +// public static native int bind(int socket, String path) +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_bind(JNIEnv *jenv, jclass jClass, jint jsocket, jstring jpath) { + errno = 0; + + struct sockaddr_un addr; + bzero(&addr, sizeof(struct sockaddr_un)); + + addr.sun_family = AF_UNIX; + + const char *nativePath = (*jenv)->GetStringUTFChars(jenv, jpath, 0); + if (nativePath == NULL) { // JVM OOM + return; + } + + if (strlen(nativePath) >= sizeof(addr.sun_path)) { + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + throwIllegalArgumentException(jenv, "Path too long"); + return; + } + + strncpy(addr.sun_path, nativePath, sizeof(addr.sun_path)); + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + + int rv = bind(jsocket, (struct sockaddr *) &addr, sizeof(addr)); + debuglog("got result from bind %d,%s\n",rv,strerror(errno)); + if (rv < 0) { + throwIOException(jenv, "Error in bind"); + return; + } +} + +// public static native void connect(int socket, String path) throws UnixSocketException; +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_connect(JNIEnv *jenv, jclass jClass, jint jsocket, jstring jpath) { + errno = 0; + + + struct sockaddr_un addr; + bzero(&addr, sizeof(struct sockaddr_un)); + addr.sun_family = AF_UNIX; + + const char *nativePath = (*jenv)->GetStringUTFChars(jenv, jpath, 0); + if (nativePath == NULL) {// JVM OOM + return; + } + + if (strlen(nativePath) >= sizeof(addr.sun_path)) { + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + throwIllegalArgumentException(jenv, "Path too long"); + return; + } + + strncpy(addr.sun_path, nativePath, sizeof(addr.sun_path)); + (*jenv)->ReleaseStringUTFChars(jenv, jpath, nativePath); + int result = connect(jsocket, (struct sockaddr *) &addr, sizeof(addr)); + debuglog("%d: got result from connect %d %s\n",jsocket,result,strerror(errno)); + if (result < 0) { + if (errno == ETIMEDOUT) { + throwSocketTimeoutException(jenv, "Socket connect timed out"); + return; + } + throwIOException(jenv, "Error in connect"); + return; + } +} + +// public static native void listen(int socket, int backlog); +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_listen(JNIEnv *jenv, jclass jClass, jint jsocket, jint jbacklog) { + errno = 0; + + int rv = listen(jsocket, jbacklog); + debuglog("got result from listen %d,%s\n",rv,strerror(errno)); + + if (rv < 0) { + throwIOException(jenv, "Error in listen"); + return; + } +} + + + +// public static native int accept(int socket, long timeoutMs) throws UnixSocketException; +// returns 0 in case that the accept timed out +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_accept(JNIEnv *jenv, jclass jClass, jint jsocket, jlong timeoutMs) { + errno = 0; + + if (timeoutMs < 0) { + throwIllegalArgumentException(jenv, "Invalid timeout"); + return -1; + } + + struct timeval startTime; + + if (gettimeofday(&startTime, NULL) < 0) { + throwIOException(jenv, "Failed to get time"); + return -1; + } + + struct timeval timeoutAbs; + timeoutAbs.tv_sec = timeoutMs / 1000; + timeoutAbs.tv_usec = (int) (timeoutMs % 1000) * 1000; + + + int rv; + + struct timeval *toPtr = NULL; + struct timeval actualTo; + + + do { + errno = 0; + + if (timeoutMs > 0) { + struct timeval nowTime, used; + if (gettimeofday(&nowTime, NULL) < 0) { + throwIOException(jenv, "Failed to get time"); + return -1; + } + + timersub(&nowTime, &startTime, &used); + timersub(&timeoutAbs, &used, &actualTo); + if (actualTo.tv_sec < 0 || (actualTo.tv_sec == 0 && actualTo.tv_usec ==0) ) { + // hit end of poll in loop + return 0; + + } + toPtr = &actualTo; + } + + fd_set set; + FD_ZERO(&set); /* clear the set */ + FD_SET(jsocket, &set); /* add our file descriptor to the set */ + + rv = select(jsocket + 1, &set, NULL, NULL, toPtr); + debuglog("XXX %d Got result from select %d : %s\n",jsocket,rv,strerror(errno)); + + if(!FD_ISSET(jsocket,&set)){ + continue; + } + } while (rv == -1 && errno == EINTR); + + if (rv < 0) { + throwIOException(jenv, "Error in select"); + return -1; + } else if (rv == 0) { + return 0; // timeout + } + + + int result; + do { + struct sockaddr_un addr; + bzero(&addr, sizeof(struct sockaddr_un)); + socklen_t rlen = sizeof(struct sockaddr_un); + result = accept(jsocket, (struct sockaddr *) &addr, &rlen); + debuglog("XXX %d Got result from accept %d : %s\n",jsocket, result,strerror(errno)); + + } while (result == -1 && errno == EINTR); + + if (result < 0) { + throwIOException(jenv, "Error in accept"); + } + return result; +} + + + +// public static native int recv(int socket, byte[] buffer, jint offset, jint length) throws UnixSocketException; +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_recv(JNIEnv *jenv, jclass jClass, jint jsocket, jbyteArray jbuf, + jint offset, jint length) { + errno = 0; + + if (offset < 0 || length <= 0) { + throwIllegalArgumentException(jenv, "Invalid offset, length"); + return -1; + } + + if (jbuf == NULL) { + throwNPE(jenv, "buffer is null"); + return -1; + } + + jint bufLen = (*jenv)->GetArrayLength(jenv, jbuf); + + if (offset >= bufLen) { + throwIllegalArgumentException(jenv, "Invalid offset, beyond end of buffer"); + return -1; + } + if (length > (bufLen - offset)) { + length = bufLen - offset; + } + + + jbyte *buf = (*jenv)->GetByteArrayElements(jenv, jbuf, NULL); + if (buf == NULL) { + return -1; + } + + + ssize_t rcount; + + do { + rcount = read(jsocket, &(buf[offset]), (size_t) length); + debuglog("XXX %d Got result from read %ld : %s\n",jsocket,rcount,strerror(errno)); + + } while (rcount == -1 && errno == EINTR); + + (*jenv)->ReleaseByteArrayElements(jenv, jbuf, buf, 0); + + + if (rcount == 0) { + // EOF in c is -1 in java + return -1; + } else if (rcount < 0) { + if (errno == EAGAIN) { + throwSocketTimeoutException(jenv, "Timeout reading from socket"); + return -1; + } + throwIOException(jenv, "Error reading from socket"); + return -1; + } + return (jint) rcount; + +} + + +// public static native int send(int socket, byte[] buffer) throws UnixSocketException; +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_send(JNIEnv *jenv, jclass jClass, jint jsocket, jbyteArray jbuf, + jint offset, jint length) { + errno = 0; + + if (offset < 0 || length <= 0) { + throwIllegalArgumentException(jenv, "Invalid offset, length or timeout"); + return -1; + } + + if (jbuf == NULL) { + throwNPE(jenv, "buffer is null"); + return -1; + } + + jint bufLen = (*jenv)->GetArrayLength(jenv, jbuf); + + + if ((offset >= bufLen) || (length > (bufLen - offset))) { + throwIllegalArgumentException(jenv, "Invalid offset or length, beyond end of buffer"); + return -1; + } + + + jbyte *buf = (*jenv)->GetByteArrayElements(jenv, jbuf, NULL); + if (buf == NULL) { // JVM OOM + return -1; + } + ssize_t wcount; + do { + wcount = write(jsocket, &(buf[offset]), (size_t) length); + debuglog("XXX %d Got result from write %ld : %s\n",jsocket,wcount,strerror(errno)); + } while (wcount == -1 && errno == EINTR); + + + (*jenv)->ReleaseByteArrayElements(jenv, jbuf, buf, 0); + + if (wcount < 0) { + if (errno == EAGAIN) { + throwSocketTimeoutException(jenv, "Timeout writing to socket"); + return -1; + } + throwIOException(jenv, "Error reading from socket"); + return -1; + } + + return (jint) wcount; + +} + + + + +// public static native close(int socket); + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_close(JNIEnv *jenv, jclass jClass, jint jsocket) { + errno = 0; + + int rv = close(jsocket); + debuglog("XXX %d got result from close %d,%s\n",jsocket,rv,strerror(errno)); + if (rv < 0) { + throwIOException(jenv, "Error in closing socket"); + return; + } +} + + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setSendBufSize(JNIEnv *jenv, jclass jClass, jint socket, + jint bufsize) { + errno = 0; + if (bufsize <= 0) { + throwIllegalArgumentException(jenv, "Invalid buffer size"); + return; + } + + int rv = setsockopt(socket, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(jint)); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setRecvBufSize(JNIEnv *jenv, jclass jClass, jint socket, + jint bufsize) { + errno = 0; + if (bufsize <= 0) { + throwIllegalArgumentException(jenv, "invalid buffer size"); + return; + } + + int rv = setsockopt(socket, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(jint)); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setSendTimeout(JNIEnv *jenv, jclass jClass, jint socket, + jint timeout) { + errno = 0; + if (timeout < 0) { + throwIllegalArgumentException(jenv, "invalid timeout"); + return; + } + + struct timeval tv; + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + + int rv = setsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(struct timeval)); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + + +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_getSendTimeout(JNIEnv *jenv, jclass jClass, jint socket) { + errno = 0; + + struct timeval tv; + bzero(&tv, sizeof(struct timeval)); + socklen_t len = sizeof(struct timeval); + + int rv = getsockopt(socket, SOL_SOCKET, SO_SNDTIMEO, &tv, &len); + debuglog("XXX %d getsockopt _getSendTimeout rv %dz\n",socket,rv); + + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return -1; + } + time_t msecs = tv.tv_sec * 1000 + tv.tv_usec / 1000; + if (msecs > INT_MAX) { + return (jint) INT_MAX; + } + return (jint) msecs; +} + + +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_setRecvTimeout(JNIEnv *jenv, jclass jClass, jint socket, + jint timeout) { + errno = 0; + if (timeout < 0) { + throwIllegalArgumentException(jenv, "Invalid timeout"); + return; + } + + struct timeval tv; + tv.tv_sec = timeout / 1000; + tv.tv_usec = (timeout % 1000) * 1000; + + int rv = setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(struct timeval)); + debuglog("XXX %d setsockopt setRecvTimeout rv %dz\n",socket,rv); + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return; + } +} + + +JNIEXPORT jint JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_getRecvTimeout(JNIEnv *jenv, jclass jClass, jint socket) { + errno = 0; + + struct timeval tv; + bzero(&tv, sizeof(struct timeval)); + socklen_t len = sizeof(struct timeval); + + int rv = getsockopt(socket, SOL_SOCKET, SO_RCVTIMEO, &tv, &len); + debuglog("XXX %d getsockopt rv %dz\n",socket,rv); + + if (rv < 0) { + throwIOException(jenv, "Error setting socket options"); + return -1; + } + + debuglog("XXX %d getsockopt _getSendTimeout rv %dz\n",socket,rv); + + time_t msecs = tv.tv_sec * 1000 + tv.tv_usec / 1000; + if (msecs > INT_MAX) { + return (jint) INT_MAX; + } + return (jint) msecs; +} + + +// public static native void shutdown(int socket, boolean input, boolean output) ; +JNIEXPORT void JNICALL +Java_com_fnproject_fn_runtime_ntv_UnixSocketNative_shutdown(JNIEnv *jenv, jclass jClass, jint jsocket, jboolean input, + jboolean output) { + errno = 0; + int how; + + if (input && output) { + how = SHUT_RDWR; + } else if (input) { + how = SHUT_RD; + } else if (output) { + how = SHUT_WR; + } else { + return; + } + + + int rv = shutdown(jsocket, how); + debuglog("XXX %d got result from shutdown %d %d,%s\n",jsocket,how,rv,strerror(errno)); + if (rv < 0) { + throwIOException(jenv, "Failed to shut down socket "); + return; + } +} diff --git a/runtime/src/main/java-filtered/com/fnproject/fn/runtime/Version.java b/runtime/src/main/java-filtered/com/fnproject/fn/runtime/Version.java new file mode 100644 index 00000000..1872aa4c --- /dev/null +++ b/runtime/src/main/java-filtered/com/fnproject/fn/runtime/Version.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime; + +/** + * This uses maven-resource filtering rather than conventional manifest versioning as it's more robust against resource changes than using standard META-INF/MANIFEST.MF + * versioning. For native image functions this negates the need for extra configuration to include manifest resources. + * + * Created on 18/02/2020. + *

+ * + * (c) 2020 Oracle Corporation + */ +public class Version { + public static final String FDK_VERSION="${project.version}"; +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultEventCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultEventCodec.java deleted file mode 100644 index 090c0c9c..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultEventCodec.java +++ /dev/null @@ -1,73 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.api.exception.FunctionOutputHandlingException; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - -/** - * DefaultEventCodec handles plain docker invocations on functions - *

- * This parses inputs from environment variables and reads and writes raw body and responses to the specified input and output streams - */ -class DefaultEventCodec implements EventCodec { - - private final Map env; - private final InputStream in; - private final OutputStream out; - - public DefaultEventCodec(Map env, InputStream in, OutputStream out) { - this.env = env; - this.in = in; - this.out = out; - } - - - private String getRequiredEnv(String name) { - String val = env.get(name); - if (val == null) { - throw new FunctionInputHandlingException("Required environment variable " + name + " is not set - are you running a function outside of fn run?"); - } - return val; - } - - @Override - public Optional readEvent() { - String method = getRequiredEnv("FN_METHOD"); - String appName = getRequiredEnv("FN_APP_NAME"); - String route = getRequiredEnv("FN_PATH"); - String requestUrl = getRequiredEnv("FN_REQUEST_URL"); - - Map headers = new HashMap<>(); - for (Map.Entry entry : env.entrySet()) { - String lowerCaseKey = entry.getKey().toLowerCase(); - if (lowerCaseKey.startsWith("fn_header_")) { - headers.put(entry.getKey().substring("fn_header_".length()), entry.getValue()); - } - } - - return Optional.of(new ReadOnceInputEvent(appName, route, requestUrl, method, in, Headers.fromMap(headers), QueryParametersParser.getParams(requestUrl))); - } - - @Override - public boolean shouldContinue() { - return false; - } - - @Override - public void writeEvent(OutputEvent evt) { - try { - evt.writeToOutput(out); - }catch(IOException e){ - throw new FunctionOutputHandlingException("error writing event",e); - } - } -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultFunctionInvocationContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultFunctionInvocationContext.java new file mode 100644 index 00000000..2ca86f5b --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultFunctionInvocationContext.java @@ -0,0 +1,15 @@ +package com.fnproject.fn.runtime; + +import java.util.List; +import java.util.Map; +import com.fnproject.fn.api.InputEvent; + +public class DefaultFunctionInvocationContext extends FunctionInvocationContext { + public DefaultFunctionInvocationContext(FunctionRuntimeContext ctx, InputEvent event) { + super(ctx, event); + } + + public Map> getAdditionalResponseHeaders() { + return super.getAdditionalResponseHeaders(); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java index 8d81dd47..0858ce95 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/DefaultMethodWrapper.java @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; -import com.fnproject.fn.api.TypeWrapper; import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.TypeWrapper; import java.lang.reflect.Method; import java.util.Arrays; @@ -14,17 +30,16 @@ public class DefaultMethodWrapper implements MethodWrapper { private final Class srcClass; private final Method srcMethod; - DefaultMethodWrapper(Class srcClass, Method srcMethod) { + public DefaultMethodWrapper(Class srcClass, Method srcMethod) { this.srcClass = srcClass; this.srcMethod = srcMethod; } public DefaultMethodWrapper(Class srcClass, String srcMethod) { - this.srcClass = srcClass; - this.srcMethod = Arrays.stream(srcClass.getMethods()) - .filter((m) -> m.getName().equals(srcMethod)) - .findFirst() - .orElseThrow(() -> new RuntimeException(new NoSuchMethodException(srcClass.getCanonicalName() + "::" + srcMethod))); + this(srcClass, Arrays.stream(srcClass.getMethods()) + .filter((m) -> m.getName().equals(srcMethod)) + .findFirst() + .orElseThrow(() -> new RuntimeException(new NoSuchMethodException(srcClass.getCanonicalName() + "::" + srcMethod)))); } @@ -40,12 +55,12 @@ public Method getTargetMethod() { @Override public TypeWrapper getParamType(int index) { - return new ParameterWrapper(this, index); + return MethodTypeWrapper.fromParameter(this, index); } @Override public TypeWrapper getReturnType() { - return new ReturnTypeWrapper(this); + return MethodTypeWrapper.fromReturnType(this); } @Override diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java b/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java index e5aa1265..bd76a6e7 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/EntryPoint.java @@ -1,18 +1,46 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; +import java.io.IOException; +import java.io.PrintStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import com.fnproject.fn.api.FnFeature; +import com.fnproject.fn.api.FnFeatures; import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.MethodWrapper; import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.RuntimeFeature; import com.fnproject.fn.api.exception.FunctionInputHandlingException; import com.fnproject.fn.api.exception.FunctionLoadException; import com.fnproject.fn.api.exception.FunctionOutputHandlingException; -import com.fnproject.fn.runtime.exception.*; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintStream; -import java.util.*; +import com.fnproject.fn.runtime.exception.FunctionInitializationException; +import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; +import com.fnproject.fn.runtime.exception.InvalidEntryPointException; /** * Main entry point @@ -20,27 +48,69 @@ public class EntryPoint { - public static void main(String... args) throws Exception { + // regex to sanitize version properties on the off chance they fall outside of acceptable header values + private static final Pattern safeVersion = Pattern.compile("[^\\w ()._-]+"); + + public static void main(String... args) { PrintStream originalSystemOut = System.out; - // Override stdout while the function is running, so that the function result can be serialized to stdout - // without interference from the user printing stuff to what they believe is stdout. System.setOut(System.err); - int exitCode = new EntryPoint().run( - System.getenv(), - System.in, - originalSystemOut, - System.err, - args); + + String format = System.getenv("FN_FORMAT"); + EventCodec codec; + if (format.equals(HTTPStreamCodec.HTTP_STREAM_FORMAT)) { + // reduce risk of confusion due to non-header like system properties + String jvmName = safeVersion.matcher(System.getProperty("java.vm.name")).replaceAll(""); + String jvmVersion = safeVersion.matcher(System.getProperty("java.version")).replaceAll(""); + String fdkVersion = "fdk-java/" + Version.FDK_VERSION + + " (jvm=" + (jvmName + ", jvmv=" + + jvmVersion + ")"); + String runtimeVersion = "java/" + jvmName + " " + jvmVersion; + + codec = new HTTPStreamCodec(System.getenv(), fdkVersion, runtimeVersion); + } else { + throw new FunctionInputHandlingException("Unsupported function format:" + format); + } + + int exitCode = new EntryPoint().run(System.getenv(), codec, System.err, args); System.setOut(originalSystemOut); System.exit(exitCode); } + + /* + * If enabled, print the logging framing content otherwise do nothing + */ + private Consumer logFramer(Map config) { + String framer = config.getOrDefault("FN_LOGFRAME_NAME", ""); + + if (!framer.isEmpty()) { + String valueSrc = config.getOrDefault("FN_LOGFRAME_HDR", ""); + + if (!valueSrc.isEmpty()) { + return (evt) -> { + String id = evt.getHeaders().get(valueSrc).orElse(""); + if (!id.isEmpty()) { + System.out.println("\n" + framer + "=" + id + "\n"); + System.err.println("\n" + framer + "=" + id + "\n"); + } + + }; + } + } + return (event) -> { + }; + } + /** - * Entrypoint runner - this executes the whole lifecycle of the fn Java FDK runtime - including multiple invocations in the function for hot functions + * Entry point runner - this executes the whole lifecycle of the fn Java FDK runtime - including multiple invocations in the function for hot functions * + * @param env the map of environment variables to run the function with (typically System.getenv but may be customised for testing) + * @param codec the codec to run the function with + * @param loggingOutput the stream to send function error/logging to - this will be wrapped into System.err within the funciton + * @param args any further args passed to the entry point - specifically the class/method name * @return the desired process exit status */ - public int run(Map env, InputStream functionInput, OutputStream functionOutput, PrintStream loggingOutput, String... args) { + public int run(Map env, EventCodec codec, PrintStream loggingOutput, String... args) { if (args.length != 1) { throw new InvalidEntryPointException("Expected one argument, of the form com.company.project.MyFunctionClass::myFunctionMethod"); } @@ -53,77 +123,110 @@ public int run(Map env, InputStream functionInput, OutputStream String cls = classMethod[0]; String mth = classMethod[1]; - int lastStatus = 0; + // TODO deprecate with default contract + final AtomicInteger lastStatus = new AtomicInteger(); try { final Map configFromEnvVars = Collections.unmodifiableMap(excludeInternalConfigAndHeaders(env)); + Consumer logFramer = logFramer(configFromEnvVars); + codec.runCodec(new EventCodec.Handler() { + FunctionRuntimeContext _runtimeContext; - FunctionLoader functionLoader = new FunctionLoader(); - FunctionRuntimeContext runtimeContext = new FunctionRuntimeContext(functionLoader.loadClass(cls, mth), configFromEnvVars); + // Create runtime context within first call to ensure that init errors are propagated + private FunctionRuntimeContext getRuntimeContext() { + if (_runtimeContext == null) { + FunctionLoader functionLoader = new FunctionLoader(); - FunctionConfigurer functionConfigurer = new FunctionConfigurer(); - functionConfigurer.configure(runtimeContext); + MethodWrapper method = functionLoader.loadClass(cls, mth); + FunctionRuntimeContext runtimeContext = new FunctionRuntimeContext(method, configFromEnvVars); + FnFeature f = method.getTargetClass().getAnnotation(FnFeature.class); + if (f != null) { + enableFeature(runtimeContext, f); + } + FnFeatures fs = method.getTargetClass().getAnnotation(FnFeatures.class); + if (fs != null) { + for (FnFeature fnFeature : fs.value()) { + enableFeature(runtimeContext, fnFeature); + } + } - String format = env.get("FN_FORMAT"); - EventCodec codec; + FunctionConfigurer functionConfigurer = new FunctionConfigurer(); + functionConfigurer.configure(runtimeContext); + _runtimeContext = runtimeContext; + } + return _runtimeContext; + } - if (format != null && format.equalsIgnoreCase("http")) { - codec = new HttpEventCodec(functionInput, functionOutput); - } else if (format == null || format.equalsIgnoreCase("default")) { - codec = new DefaultEventCodec(env, functionInput, functionOutput); - } else { - throw new FunctionInputHandlingException("Unsupported function format:" + format); - } + @Override + public OutputEvent handle(InputEvent evt) { + try { + // output log frame prior to any user code execution + logFramer.accept(evt); + FunctionRuntimeContext runtimeContext = getRuntimeContext(); + FunctionInvocationContext fic = runtimeContext.newInvocationContext(evt); + try (InputEvent myEvt = evt) { + OutputEvent output = runtimeContext.tryInvoke(evt, fic); + if (output == null) { + throw new FunctionInputHandlingException("No invoker found for input event"); + } + if (output.isSuccess()) { + lastStatus.set(0); + fic.fireOnSuccessfulInvocation(); + } else { + lastStatus.set(1); + fic.fireOnFailedInvocation(); + } - do { - try { - Optional evtOpt = codec.readEvent(); - if (!evtOpt.isPresent()) { - break; - } + return output.withHeaders(output.getHeaders().setHeaders(fic.getAdditionalResponseHeaders())); - FunctionInvocationContext fic = runtimeContext.newInvocationContext(); - try (InputEvent evt = evtOpt.get()) { - OutputEvent output = runtimeContext.tryInvoke(evt, fic); - if (output == null) { - throw new FunctionInputHandlingException("No invoker found for input event"); - } - codec.writeEvent(output); - if (output.isSuccess()) { - lastStatus = 0; - fic.fireOnSuccessfulInvocation(); - } else { - lastStatus = 1; + + } catch (IOException err) { + fic.fireOnFailedInvocation(); + throw new FunctionInputHandlingException("Error closing function input", err); + } catch (Exception e) { + // Make sure we commit any pending Flows, then rethrow fic.fireOnFailedInvocation(); + throw e; } - } catch (IOException e) { - fic.fireOnFailedInvocation(); - throw new FunctionInputHandlingException("Error closing function input", e); - } catch (Exception e) { - // Make sure we commit any pending Flows, then rethrow - fic.fireOnFailedInvocation(); - throw e; + } catch (InternalFunctionInvocationException fie) { + loggingOutput.println("An error occurred in function: " + filterStackTraceToOnlyIncludeUsersCode(fie)); + loggingOutput.flush(); + // Here: completer-invoked continuations are *always* reported as successful to the Fn platform; + // the completer interprets the embedded HTTP-framed response. + lastStatus.set(fie.toOutput().isSuccess() ? 0 : 1); + return fie.toOutput(); + } catch (FunctionLoadException | FunctionInputHandlingException | FunctionOutputHandlingException e) { + // catch all block; + loggingOutput.println(filterStackTraceToOnlyIncludeUsersCode(e)); + loggingOutput.flush(); + lastStatus.set(2); + return new InternalFunctionInvocationException("Error initializing function", e).toOutput(); } - - } catch (InternalFunctionInvocationException fie) { - loggingOutput.println("An error occurred in function: " + filterStackTraceToOnlyIncludeUsersCode(fie)); - codec.writeEvent(fie.toOutput()); - - // Here: completer-invoked continuations are *always* reported as successful to the Fn platform; - // the completer interprets the embedded HTTP-framed response. - lastStatus = fie.toOutput().isSuccess() ? 0 : 1; } - } while (codec.shouldContinue()); - } catch (FunctionLoadException | FunctionInputHandlingException | FunctionOutputHandlingException e) { - // catch all block; - loggingOutput.println(filterStackTraceToOnlyIncludeUsersCode(e)); - return 2; - } catch (Exception ee){ + + }); + } catch (Exception ee) { loggingOutput.println("An unexpected error occurred:"); ee.printStackTrace(loggingOutput); return 1; } - return lastStatus; + return lastStatus.get(); + } + + private void enableFeature(FunctionRuntimeContext runtimeContext, FnFeature f) { + RuntimeFeature rf; + try { + Class featureClass = f.value(); + rf = featureClass.newInstance(); + } catch (Exception e) { + throw new FunctionInitializationException("Could not load feature class " + f.value().toString(), e); + } + + try { + rf.initialize(runtimeContext); + } catch (Exception e) { + throw new FunctionInitializationException("Exception while calling initialization on runtime feature " + f.value(), e); + } } @@ -158,14 +261,14 @@ private void addExceptionToStringBuilder(StringBuilder sb, Throwable t) { // This elides the FQCN of the exception class if it's from our runtime. sb.append(t.getMessage()); } else { - sb.append("Caused by: " + t.toString()); + sb.append("Caused by: ").append(t.toString()); } for (StackTraceElement elem : t.getStackTrace()) { if (elem.getClassName().startsWith("com.fnproject.fn")) { break; } - sb.append("\n at " + elem.toString()); + sb.append("\n at ").append(elem.toString()); } sb.append("\n"); @@ -177,8 +280,8 @@ private void addExceptionToStringBuilder(StringBuilder sb, Throwable t) { * any headers that were added to env. Headers are identified as being variables prepended with 'HEADER_'. */ private Map excludeInternalConfigAndHeaders(Map env) { - Set nonConfigEnvKeys = new HashSet<>(Arrays.asList("fn_app_name", "fn_path", "fn_method", "fn_request_url", - "fn_format", "content-length", "fn_call_id")); + Set nonConfigEnvKeys = new HashSet<>(Arrays.asList("fn_path", "fn_method", "fn_request_url", + "fn_format", "content-length", "fn_call_id")); Map config = new HashMap<>(); for (Map.Entry entry : env.entrySet()) { String lowerCaseKey = entry.getKey().toLowerCase(); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java index 56408b7d..1f3c46e8 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/EventCodec.java @@ -1,35 +1,51 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.api.InputEvent; import com.fnproject.fn.api.OutputEvent; -import java.io.IOException; -import java.util.Optional; - /** * Event Codec - deals with different calling conventions between fn and the function docker container */ public interface EventCodec { - /** - * Read a event from the input - * - * @return an empty input stream if the end of the stream is reached or an event if otherwise - */ - Optional readEvent(); /** - * Should the codec be used again - * - * @return true if {@link #readEvent()} can read another message + * Handler handles function content based on codec events + *

+ * A handler should generally deal with all exceptions (except errors) and convert them into appropriate OutputEvents */ - boolean shouldContinue(); + interface Handler { + /** + * Handle a function input event and generate a response + * + * @param event the event to handle + * @return an output event indicating the result of calling a function or an error + */ + OutputEvent handle(InputEvent event); + } /** - * Write an event to the output + * Run Codec should continuously run the function event loop until either the FDK should exit normally (returning normally) or an error occurred. + *

+ * Codec should invoke the handler for each received event * - * @param evt event to write - * @throws IOException if an error occurs while writing + * @param h the handler to run */ - void writeEvent(OutputEvent evt); + void runCodec(Handler h); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java index e61934ab..7f355176 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionConfigurer.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.api.FnConfiguration; @@ -7,7 +23,9 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Optional; /** * Loads function entry points based on their class name and method name creating a {@link FunctionRuntimeContext} @@ -19,7 +37,6 @@ public class FunctionConfigurer { * create a function runtime context for a given class and method name * * @param runtimeContext The runtime context encapsulating the function to be run - * @return a new runtime context */ public void configure(FunctionRuntimeContext runtimeContext) { validateConfigurationMethods(runtimeContext.getMethodWrapper()); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java index 4d51637a..7f8ee6ba 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationCallback.java @@ -1,7 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; public interface FunctionInvocationCallback { - void fireOnSuccessfulInvocation(); void fireOnFailedInvocation(); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java index c828bf83..3cd8595d 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionInvocationContext.java @@ -1,9 +1,28 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; import com.fnproject.fn.api.InvocationContext; import com.fnproject.fn.api.InvocationListener; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; /** @@ -12,10 +31,14 @@ */ public class FunctionInvocationContext implements InvocationContext, FunctionInvocationCallback { private final FunctionRuntimeContext runtimeContext; - private List invocationListeners = new CopyOnWriteArrayList<>(); + private final List invocationListeners = new CopyOnWriteArrayList<>(); + + private final InputEvent event; + private final Map> additionalResponseHeaders = new ConcurrentHashMap<>(); - public FunctionInvocationContext(FunctionRuntimeContext ctx) { + FunctionInvocationContext(FunctionRuntimeContext ctx, InputEvent event) { this.runtimeContext = ctx; + this.event = event; } @Override @@ -28,12 +51,52 @@ public void addListener(InvocationListener listener) { invocationListeners.add(listener); } + @Override + public Headers getRequestHeaders() { + return event.getHeaders(); + } + + @Override + public void addResponseHeader(String key, String value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + + additionalResponseHeaders.merge(key, Collections.singletonList(value), (a, b) -> { + List l = new ArrayList<>(a); + l.addAll(b); + return l; + }); + } + + /** + * returns the internal map of added response headers + * + * @return mutable map of internal response headers + */ + Map> getAdditionalResponseHeaders() { + return additionalResponseHeaders; + } + + @Override + public void setResponseHeader(String key, String value, String... vs) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(vs, "vs"); + Arrays.stream(vs).forEach(v->Objects.requireNonNull(v,"null value in list ")); + + String cKey = Headers.canonicalKey(key); + if (value == null) { + additionalResponseHeaders.remove(cKey); + return; + } + additionalResponseHeaders.put(cKey, Collections.singletonList(value)); + } + @Override public void fireOnSuccessfulInvocation() { for (InvocationListener listener : invocationListeners) { try { listener.onSuccess(); - } catch (Exception e) { + } catch (Exception ignored) { } } } @@ -43,7 +106,7 @@ public void fireOnFailedInvocation() { for (InvocationListener listener : invocationListeners) { try { listener.onFailure(); - } catch (Exception e) { + } catch (Exception ignored) { } } } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java index 092645a5..c2bb23cd 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionLoader.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.api.MethodWrapper; @@ -20,6 +36,8 @@ public class FunctionLoader { */ public MethodWrapper loadClass(String className, String fnName) { Class targetClass = loadClass(className); + + return new DefaultMethodWrapper(targetClass, getTargetMethod(targetClass, fnName)); } @@ -66,7 +84,7 @@ private Class loadClass(String className) { * Override the classloader used for fn class resolution * Primarily for testing, otherwise the system/default classloader is used. * - * @param loader + * @param loader the context class loader to use for this function */ public static void setContextClassLoader(ClassLoader loader) { contextClassLoader = loader; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java index 1425b7f0..cdc5b313 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/FunctionRuntimeContext.java @@ -1,50 +1,46 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; -import com.fnproject.fn.api.FunctionInvoker; -import com.fnproject.fn.api.InputBinding; -import com.fnproject.fn.api.InputCoercion; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.InvocationContext; -import com.fnproject.fn.api.MethodWrapper; -import com.fnproject.fn.api.OutputBinding; -import com.fnproject.fn.api.OutputCoercion; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.RuntimeContext; -import com.fnproject.fn.runtime.coercion.ByteArrayCoercion; -import com.fnproject.fn.runtime.coercion.InputEventCoercion; -import com.fnproject.fn.runtime.coercion.OutputEventCoercion; -import com.fnproject.fn.runtime.coercion.StringCoercion; -import com.fnproject.fn.runtime.coercion.VoidCoercion; -import com.fnproject.fn.runtime.coercion.jackson.JacksonCoercion; -import com.fnproject.fn.runtime.exception.FunctionClassInstantiationException; +import com.fnproject.fn.api.*; import com.fnproject.fn.api.exception.FunctionConfigurationException; import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.runtime.flow.FlowContinuationInvoker; +import com.fnproject.fn.runtime.coercion.*; +import com.fnproject.fn.runtime.coercion.jackson.JacksonCoercion; +import com.fnproject.fn.runtime.exception.FunctionClassInstantiationException; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; public class FunctionRuntimeContext implements RuntimeContext { private final Map config; private final MethodWrapper method; - private Map attributes = new HashMap<>(); - private List configuredInvokers = new ArrayList<>(); + private final Map attributes = new HashMap<>(); + private final List preCallHandlers = new ArrayList<>(); + private final List configuredInvokers = new ArrayList<>(); private Object instance; - private final List builtinInputCoercions = Arrays.asList(new StringCoercion(), new ByteArrayCoercion(), new InputEventCoercion(), JacksonCoercion.instance()); + private final List builtinInputCoercions = Arrays.asList(new ContextCoercion(), new StringCoercion(), new ByteArrayCoercion(), new InputEventCoercion(), JacksonCoercion.instance()); private final List userInputCoercions = new LinkedList<>(); private final List builtinOutputCoercions = Arrays.asList(new StringCoercion(), new ByteArrayCoercion(), new VoidCoercion(), new OutputEventCoercion(), JacksonCoercion.instance()); private final List userOutputCoercions = new LinkedList<>(); @@ -52,7 +48,27 @@ public class FunctionRuntimeContext implements RuntimeContext { public FunctionRuntimeContext(MethodWrapper method, Map config) { this.method = method; this.config = Objects.requireNonNull(config); - configuredInvokers.addAll(Arrays.asList(new FlowContinuationInvoker(), new MethodFunctionInvoker())); + configuredInvokers.add(new MethodFunctionInvoker()); + } + + @Override + public String getAppID() { + return config.getOrDefault("FN_APP_ID", ""); + } + + @Override + public String getFunctionID() { + return config.getOrDefault("FN_FN_ID", ""); + } + + @Override + public String getAppName() { + return config.getOrDefault("FN_APP_NAME", ""); + } + + @Override + public String getFunctionName() { + return config.getOrDefault("FN_FN_NAME", ""); } @Override @@ -69,7 +85,7 @@ public Optional getInvokeInstance() { if (RuntimeContext.class.isAssignableFrom(ctor.getParameterTypes()[0])) { instance = ctor.newInstance(FunctionRuntimeContext.this); } else { - if ( getMethod().getTargetClass().getEnclosingClass() != null && ! Modifier.isStatic(getMethod().getTargetClass().getModifiers()) ) { + if (getMethod().getTargetClass().getEnclosingClass() != null && !Modifier.isStatic(getMethod().getTargetClass().getModifiers())) { throw new FunctionClassInstantiationException("The function " + getMethod().getTargetClass() + " cannot be instantiated as it is a non-static inner class"); } else { throw new FunctionClassInstantiationException("The function " + getMethod().getTargetClass() + " cannot be instantiated as its constructor takes an unrecognized argument of type " + constructors[0].getParameterTypes()[0] + ". Function classes should have a single public constructor that takes either no arguments or a RuntimeContext argument"); @@ -131,11 +147,11 @@ public void addInputCoercion(InputCoercion ic) { public List getInputCoercions(MethodWrapper targetMethod, int param) { Annotation parameterAnnotations[] = targetMethod.getTargetMethod().getParameterAnnotations()[param]; Optional coercionAnnotation = Arrays.stream(parameterAnnotations) - .filter((ann) -> ann.annotationType().equals(InputBinding.class)) - .findFirst(); + .filter((ann) -> ann.annotationType().equals(InputBinding.class)) + .findFirst(); if (coercionAnnotation.isPresent()) { try { - List coercionList = new ArrayList(); + List coercionList = new ArrayList<>(); InputBinding inputBindingAnnotation = (InputBinding) coercionAnnotation.get(); coercionList.add(inputBindingAnnotation.coercion().getDeclaredConstructor().newInstance()); return coercionList; @@ -154,9 +170,19 @@ public void addOutputCoercion(OutputCoercion oc) { userOutputCoercions.add(Objects.requireNonNull(oc)); } + @Override - public void setInvoker(FunctionInvoker invoker) { - configuredInvokers.add(1, invoker); + public void addInvoker(FunctionInvoker invoker, FunctionInvoker.Phase phase) { + switch (phase) { + case PreCall: + preCallHandlers.add(0, invoker); + break; + case Call: + configuredInvokers.add(0, invoker); + break; + default: + throw new IllegalArgumentException("Unsupported phase " + phase); + } } @Override @@ -164,20 +190,25 @@ public MethodWrapper getMethod() { return method; } - public FunctionInvocationContext newInvocationContext() { - return new FunctionInvocationContext(this); + public FunctionInvocationContext newInvocationContext(InputEvent inputEvent) { + return new FunctionInvocationContext(this, inputEvent); } public OutputEvent tryInvoke(InputEvent evt, InvocationContext entryPoint) { - OutputEvent output = null; + for (FunctionInvoker invoker : preCallHandlers) { + Optional result = invoker.tryInvoke(entryPoint, evt); + if (result.isPresent()) { + return result.get(); + } + } + for (FunctionInvoker invoker : configuredInvokers) { Optional result = invoker.tryInvoke(entryPoint, evt); if (result.isPresent()) { - output = result.get(); - break; + return result.get(); } } - return output; + return null; } @Override @@ -185,7 +216,7 @@ public List getOutputCoercions(Method method) { OutputBinding coercionAnnotation = method.getAnnotation(OutputBinding.class); if (coercionAnnotation != null) { try { - List coercionList = new ArrayList(); + List coercionList = new ArrayList<>(); coercionList.add(coercionAnnotation.coercion().getDeclaredConstructor().newInstance()); return coercionList; @@ -193,7 +224,7 @@ public List getOutputCoercions(Method method) { throw new FunctionConfigurationException("Unable to instantiate output coercion class for method " + getMethod()); } } - List outputList = new ArrayList(); + List outputList = new ArrayList<>(); outputList.addAll(userOutputCoercions); outputList.addAll(builtinOutputCoercions); return outputList; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/HTTPStreamCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/HTTPStreamCodec.java new file mode 100644 index 00000000..4b442783 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/HTTPStreamCodec.java @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime; + + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermissions; +import java.time.Instant; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.fasterxml.jackson.core.io.CharTypes; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.exception.FunctionInputHandlingException; +import com.fnproject.fn.api.exception.FunctionOutputHandlingException; +import com.fnproject.fn.runtime.exception.FunctionIOException; +import com.fnproject.fn.runtime.exception.FunctionInitializationException; +import com.fnproject.fn.runtime.ntv.UnixServerSocket; +import com.fnproject.fn.runtime.ntv.UnixSocket; +import org.apache.http.Header; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.HttpVersion; +import org.apache.http.ParseException; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.DefaultBHttpServerConnection; +import org.apache.http.impl.io.EmptyInputStream; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpService; +import org.apache.http.protocol.ImmutableHttpProcessor; +import org.apache.http.protocol.UriHttpRequestHandlerMapper; + +/** + * Fn HTTP Stream over Unix domain sockets codec + *

+ *

+ * This creates a new unix socket on the address specified by env["FN_LISTENER"] - and accepts requests. + *

+ * This currently only handles exactly one concurrent connection + *

+ * Created on 24/08/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public final class HTTPStreamCodec implements EventCodec, Closeable { + + public static final String HTTP_STREAM_FORMAT = "http-stream"; + private static final String FN_LISTENER = "FN_LISTENER"; + private static final Set stripInputHeaders; + private static final Set stripOutputHeaders; + private final Map env; + private final String fdkVersion; + private final String runtimeVersion; + private final AtomicBoolean stopping = new AtomicBoolean(false); + private final File socketFile; + private final CompletableFuture stopped = new CompletableFuture<>(); + private final UnixServerSocket socket; + private final File tempFile; + + + static { + Set hin = new HashSet<>(); + hin.add("Host"); + hin.add("Accept-Encoding"); + hin.add("Transfer-Encoding"); + hin.add("User-Agent"); + hin.add("Connection"); + hin.add("TE"); + + stripInputHeaders = Collections.unmodifiableSet(hin); + + Set hout = new HashSet<>(); + hout.add("Content-Length"); + hout.add("Transfer-Encoding"); + hout.add("Connection"); + hout.add("Fn-Fdk-Version"); + + stripOutputHeaders = Collections.unmodifiableSet(hout); + } + + + private String randomString() { + int leftLimit = 97; + int rightLimit = 122; + int targetStringLength = 10; + Random random = new Random(); + StringBuilder buffer = new StringBuilder(targetStringLength); + for (int i = 0; i < targetStringLength; i++) { + int randomLimitedInt = leftLimit + (int) + (random.nextFloat() * (rightLimit - leftLimit + 1)); + buffer.append((char) randomLimitedInt); + } + return buffer.toString(); + } + + + /** + * Construct a new HTTPStreamCodec based on the environment + * + * @param env an env map + * @param fdkVersion the version to report to the runtime + * @param runtimeVersion underlying JVM version to report to the runtime + */ + HTTPStreamCodec(Map env, String fdkVersion, String runtimeVersion) { + this.env = Objects.requireNonNull(env, "env"); + this.fdkVersion = Objects.requireNonNull(fdkVersion, "fdkVersion"); + this.runtimeVersion = Objects.requireNonNull(runtimeVersion, "runtimeVersion"); + String listenerAddress = getRequiredEnv(FN_LISTENER); + + if (!listenerAddress.startsWith("unix:/")) { + throw new FunctionInitializationException("Invalid listener address - it should start with unix:/ :'" + listenerAddress + "'"); + } + String listenerFile = listenerAddress.substring("unix:".length()); + + socketFile = new File(listenerFile); + + + UnixServerSocket serverSocket = null; + File listenerDir = socketFile.getParentFile(); + tempFile = new File(listenerDir, randomString() + ".sock"); + try { + + serverSocket = UnixServerSocket.listen(tempFile.getAbsolutePath(), 1); + // Adjust socket permissions and move file + Files.setPosixFilePermissions(tempFile.toPath(), PosixFilePermissions.fromString("rw-rw-rw-")); + Files.createSymbolicLink(socketFile.toPath(), tempFile.toPath().getFileName()); + + this.socket = serverSocket; + } catch (IOException e) { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException ignored) { + } + + } + throw new FunctionInitializationException("Unable to bind to unix socket in " + socketFile, e); + } + + + } + + + private String jsonError(String message, String detail) { + if (message == null) { + message = ""; + } + + StringBuilder sb = new StringBuilder(); + sb.append("{ \"message\":\""); + CharTypes.appendQuoted(sb, message); + sb.append("\""); + + if (detail != null) { + sb.append(", \"detail\":\""); + CharTypes.appendQuoted(sb, detail); + sb.append("\""); + } + + sb.append("}"); + return sb.toString(); + } + + @Override + public void runCodec(Handler h) { + + UriHttpRequestHandlerMapper mapper = new UriHttpRequestHandlerMapper(); + mapper.register("/call", ((request, response, context) -> { + InputEvent evt; + try { + evt = readEvent(request); + } catch (FunctionInputHandlingException e) { + response.setStatusCode(500); + response.setEntity(new StringEntity(jsonError("Invalid input for function", e.getMessage()), ContentType.APPLICATION_JSON)); + return; + } + + OutputEvent outEvt; + + try { + outEvt = h.handle(evt); + } catch (Exception e) { + response.setStatusCode(500); + response.setEntity(new StringEntity(jsonError("Unhandled internal error in FDK", e.getMessage()), ContentType.APPLICATION_JSON)); + return; + } + + try { + writeEvent(outEvt, response); + } catch (Exception e) { + // TODO strange edge cases might appear with headers where the response is half written here + response.setStatusCode(500); + response.setEntity(new StringEntity(jsonError("Unhandled internal error while writing FDK response", e.getMessage()), ContentType.APPLICATION_JSON)); + } + } + )); + + ImmutableHttpProcessor requestProcess = new ImmutableHttpProcessor(new HttpRequestInterceptor[0], new HttpResponseInterceptor[0]); + HttpService svc = new HttpService(requestProcess, mapper); + + try { + + while (!stopping.get()) { + try (UnixSocket sock = socket.accept(100)) { + if (sock == null) { + // timeout during accept, try again + continue; + } + // TODO tweak these properly + sock.setSendBufferSize(65535); + sock.setReceiveBufferSize(65535); + + + if (stopping.get()) { + // ignore IO errors on stop + return; + } + try { + DefaultBHttpServerConnection con = new DefaultBHttpServerConnection(65535); + con.bind(sock); + while (!sock.isClosed()) { + try { + svc.handleRequest(con, new BasicHttpContext()); + } catch (HttpException e) { + sock.close(); + throw e; + } + } + } catch (HttpException | IOException e) { + System.err.println("FDK Got Exception while handling HTTP request" + e.getMessage()); + e.printStackTrace(); + // we continue here and leave the container hot + } + } catch (IOException e) { + if (stopping.get()) { + // ignore IO errors on stop + return; + } + throw new FunctionIOException("failed to accept connection from platform, terminating", e); + } + + } + } finally { + stopped.complete(true); + } + + + } + + + private String getRequiredEnv(String name) { + String val = env.get(name); + if (val == null) { + throw new FunctionInputHandlingException("Required environment variable " + name + " is not set - are you running a function outside of fn run?"); + } + return val; + } + + private static String getRequiredHeader(HttpRequest request, String headerName) { + Header header = request.getFirstHeader(headerName); + if (header == null) { + throw new FunctionInputHandlingException("Required FDK header variable " + headerName + " is not set, check you are using the latest fn and FDK versions"); + } + return header.getValue(); + } + + private InputEvent readEvent(HttpRequest request) { + + InputStream bodyStream; + if (request instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest entityEnclosingRequest = (HttpEntityEnclosingRequest) request; + try { + bodyStream = entityEnclosingRequest.getEntity().getContent(); + } catch (IOException exception) { + throw new FunctionInputHandlingException("error handling input", exception); + } + } else { + bodyStream = EmptyInputStream.INSTANCE; + } + + + String deadline = getRequiredHeader(request, "Fn-Deadline"); + String callID = getRequiredHeader(request, "Fn-Call-Id"); + + if (callID == null) { + callID = ""; + } + Instant deadlineDate = Instant.now().plus(1, ChronoUnit.HOURS); + if (deadline != null) { + try { + deadlineDate = Instant.parse(deadline); + } catch (DateTimeParseException e) { + throw new FunctionInputHandlingException("Invalid deadline date format", e); + } + } + Headers headersIn = Headers.emptyHeaders(); + + + for (Header h : request.getAllHeaders()) { + if (stripInputHeaders.contains(Headers.canonicalKey(h.getName()))) { + continue; + } + headersIn = headersIn.addHeader(h.getName(), h.getValue()); + } + + return new ReadOnceInputEvent(bodyStream, headersIn, callID, deadlineDate); + + } + + private void writeEvent(OutputEvent evt, HttpResponse response) { + + evt.getHeaders().asMap() + .entrySet() + .stream() + .filter(e -> !stripOutputHeaders.contains(e.getKey())) + .flatMap(e -> e.getValue().stream().map((v) -> new BasicHeader(e.getKey(), v))) + .forEachOrdered(response::addHeader); + + ContentType contentType = evt.getContentType().map(c -> { + try { + return ContentType.parse(c); + } catch (ParseException e) { + return ContentType.DEFAULT_BINARY; + } + }).orElse(ContentType.DEFAULT_BINARY); + + response.setHeader("Content-Type", contentType.toString()); + response.setHeader("Fn-Fdk-Version", fdkVersion); + response.setHeader("Fn-Fdk-Runtime", runtimeVersion); + + response.setStatusLine(new BasicStatusLine(HttpVersion.HTTP_1_1, evt.getStatus().getCode(), evt.getStatus().name())); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + // TODO remove output buffering here - possibly change OutputEvent contract to support providing an InputStream? + try { + evt.writeToOutput(bos); + } catch (IOException e) { + throw new FunctionOutputHandlingException("Error writing output", e); + } + byte[] data = bos.toByteArray(); + response.setEntity(new ByteArrayEntity(data, contentType)); + + } + + + @Override + public void close() throws IOException { + if (stopping.compareAndSet(false, true)) { + socket.close(); + + try { + stopped.get(); + } catch (Exception ignored) { + } + socketFile.delete(); + tempFile.delete(); + } + + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/HttpEventCodec.java b/runtime/src/main/java/com/fnproject/fn/runtime/HttpEventCodec.java deleted file mode 100644 index 3c78a6ee..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/HttpEventCodec.java +++ /dev/null @@ -1,145 +0,0 @@ -package com.fnproject.fn.runtime; - - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.api.exception.FunctionOutputHandlingException; -import org.apache.http.Header; -import org.apache.http.HttpException; -import org.apache.http.HttpRequest; -import org.apache.http.ProtocolVersion; -import org.apache.http.config.MessageConstraints; -import org.apache.http.impl.io.*; -import org.apache.http.io.HttpMessageParser; -import org.apache.http.io.SessionInputBuffer; -import org.apache.http.io.SessionOutputBuffer; -import org.apache.http.message.BasicHttpResponse; -import org.apache.http.message.BasicStatusLine; - -import java.io.*; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; - - -/** - * Reads input via an InputStream as an HTTP request. - *

- * This does not consume the whole event from the buffer, The caller is responsible for ensuring that either {@link InputEvent#consumeBody(Function)} or {@link InputEvent#close()} is called before reading a new event - */ -public class HttpEventCodec implements EventCodec { - - private static final String CONTENT_TYPE_HEADER = "Content-Type"; - private final SessionInputBuffer sib; - private final SessionOutputBuffer sob; - private final HttpMessageParser parser; - - - HttpEventCodec(InputStream input, OutputStream output) { - - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sib.bind(Objects.requireNonNull(input)); - this.sib = sib; - - SessionOutputBufferImpl sob = new SessionOutputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sob.bind(output); - this.sob = sob; - - parser = new DefaultHttpRequestParserFactory(null, null).create(sib, MessageConstraints.custom().setMaxHeaderCount(65535).setMaxLineLength(65535).build()); - } - - private static String requiredHeader(HttpRequest req, String id) { - return Optional.ofNullable(req.getFirstHeader(id)).map(Header::getValue).orElseThrow(() -> new FunctionInputHandlingException("Incoming HTTP frame is missing required header: " + id)); - } - - @Override - public final Optional readEvent() { - - HttpRequest req; - try { - req = parser.parse(); - } catch (org.apache.http.ConnectionClosedException e) { - // End of stream - signal normal termination - return Optional.empty(); - } catch (IOException | HttpException e) { - throw new FunctionInputHandlingException("Failed to read HTTP content from input", e); - } - - InputStream bodyStream; - if (req.getHeaders("content-length").length > 0) { - long contentLength = Long.parseLong(requiredHeader(req, "content-length")); - bodyStream = new ContentLengthInputStream(sib, contentLength); - } else if (req.getHeaders("transfer-encoding").length > 0 && - req.getFirstHeader("transfer-encoding").getValue().equalsIgnoreCase("chunked")) { - bodyStream = new ChunkedInputStream(sib); - } else { - bodyStream = new ByteArrayInputStream(new byte[]{}); - } - String appName = requiredHeader(req, "fn_app_name"); - String route = requiredHeader(req, "fn_path"); - String method = requiredHeader(req, "fn_method"); - String requestUrl = requiredHeader(req, "fn_request_url"); - - Map headers = new HashMap<>(); - for (Header h : req.getAllHeaders()) { - headers.put(h.getName(), h.getValue()); - } - - return Optional.of(new ReadOnceInputEvent(appName, route, requestUrl, method, - bodyStream, Headers.fromMap(headers), - QueryParametersParser.getParams(requestUrl))); - - } - - @Override - public boolean shouldContinue() { - return true; - } - - @Override - public void writeEvent(OutputEvent evt) { - try { - // TODO: We buffer the whole output here just to get the content-length - // TODO: functions should support chunked - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - evt.writeToOutput(bos); - - byte[] data = bos.toByteArray(); - - BasicHttpResponse response; - - if (evt.isSuccess()) { - response = new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), evt.getStatusCode(), "INVOKED")); - } else { - response = new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), evt.getStatusCode(), "INVOKE FAILED")); - } - - evt.getHeaders().getAll().forEach(response::setHeader); - evt.getContentType().ifPresent((ct) -> response.setHeader(CONTENT_TYPE_HEADER, ct)); - response.setHeader("Content-length", String.valueOf(data.length)); - - - DefaultHttpResponseWriter writer = new DefaultHttpResponseWriter(sob); - try { - writer.write(response); - } catch (HttpException e) { - throw new FunctionOutputHandlingException("Failed to write response", e); - } - ContentLengthOutputStream clos = new ContentLengthOutputStream(sob, data.length); - clos.write(data); - clos.flush(); - clos.close(); - sob.flush(); - - } catch (IOException e) { - throw new FunctionOutputHandlingException("Failed to write output to stream", e); - } - } - -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java b/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java index 30f95986..0729ff37 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/MethodFunctionInvoker.java @@ -1,14 +1,35 @@ -package com.fnproject.fn.runtime; +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.fnproject.fn.runtime; -import com.fnproject.fn.api.*; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; -import com.fnproject.fn.api.exception.FunctionOutputHandlingException; import java.lang.reflect.InvocationTargetException; import java.util.Optional; +import com.fnproject.fn.api.FunctionInvoker; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.exception.FunctionInputHandlingException; +import com.fnproject.fn.api.exception.FunctionOutputHandlingException; +import com.fnproject.fn.runtime.exception.InternalFunctionInvocationException; + /** * Method function invoker *

@@ -17,6 +38,7 @@ */ public class MethodFunctionInvoker implements FunctionInvoker { + /** * Invoke the function wrapped by this loader * @@ -33,6 +55,7 @@ public Optional tryInvoke(InvocationContext ctx, InputEvent evt) th Object rawResult; + try { rawResult = method.getTargetMethod().invoke(ctx.getRuntimeContext().getInvokeInstance().orElse(null), userFunctionParams); } catch (IllegalAccessException | InvocationTargetException e) { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java index e7743a72..a0f243f3 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/MethodTypeWrapper.java @@ -1,18 +1,32 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; -import com.fnproject.fn.api.TypeWrapper; import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.api.TypeWrapper; import net.jodah.typetools.TypeResolver; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -public abstract class MethodTypeWrapper implements TypeWrapper { - protected final MethodWrapper src; - protected Class parameterClass; +public final class MethodTypeWrapper implements TypeWrapper { + private final Class parameterClass; - public MethodTypeWrapper(MethodWrapper src, Class parameterClass) { - this.src = src; + private MethodTypeWrapper(Class parameterClass) { this.parameterClass = parameterClass; } @@ -21,7 +35,7 @@ public Class getParameterClass() { return parameterClass; } - protected static Class resolveType(Type type, MethodWrapper src) { + static Class resolveType(Type type, MethodWrapper src) { if (type instanceof Class) { return PrimitiveTypeResolver.resolve((Class) type); } else if (type instanceof ParameterizedType) { @@ -36,4 +50,13 @@ protected static Class resolveType(Type type, MethodWrapper src) { } } + public static TypeWrapper fromParameter(MethodWrapper method, int paramIndex) { + return new MethodTypeWrapper(resolveType(method.getTargetMethod().getGenericParameterTypes()[paramIndex], method)); + } + + public static TypeWrapper fromReturnType(MethodWrapper method) { + return new MethodTypeWrapper(resolveType(method.getTargetMethod().getGenericReturnType(), method)); + + } + } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ParameterWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/ParameterWrapper.java deleted file mode 100644 index efe099a4..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/ParameterWrapper.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.MethodWrapper; - -/** - * A {@link com.fnproject.fn.api.TypeWrapper} for capturing type information about a method's parameter. - */ -class ParameterWrapper extends MethodTypeWrapper { - - /** - * Constructor - * - * @param method the method - * @param paramIndex the index of the parameter which we store type information about - */ - public ParameterWrapper(MethodWrapper method, int paramIndex) { - super(method, resolveType(method.getTargetMethod().getGenericParameterTypes()[paramIndex], method)); - } - - @Override - public Class getParameterClass() { - return parameterClass; - } -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java b/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java index 6c3e66a2..b5e6dea1 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/PrimitiveTypeResolver.java @@ -1,10 +1,26 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import java.util.HashMap; import java.util.Map; public class PrimitiveTypeResolver { - private static Map, Class> boxedTypes = new HashMap<>(); + private static final Map, Class> boxedTypes = new HashMap<>(); static { boxedTypes.put(void.class, Void.class); boxedTypes.put(boolean.class, Boolean.class); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersImpl.java b/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersImpl.java deleted file mode 100644 index bf7309fc..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersImpl.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.QueryParameters; - -import java.util.*; - -public class QueryParametersImpl implements QueryParameters { - private final Map> params; - - public QueryParametersImpl() { - this.params = new HashMap<>(); - } - - public QueryParametersImpl(Map> params) { - this.params = Objects.requireNonNull(params); - } - - public Optional get(String key) { - Objects.requireNonNull(key); - return Optional.of(getValues(key)) - .filter((values) -> values.size() > 0) - .flatMap((values) -> Optional.ofNullable(values.get(0))); - } - - public List getValues(String key) { - Objects.requireNonNull(key); - List values = this.params.get(key); - if (values == null) { - return Collections.emptyList(); - } - return values; - } - - public int size() { - return params.size(); - } - - @Override - public Map> getAll() { - return new HashMap<>(params); - } -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java b/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java index c120b6bd..f9cac12e 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ReadOnceInputEvent.java @@ -1,13 +1,29 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.api.Headers; import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.QueryParameters; import com.fnproject.fn.api.exception.FunctionInputHandlingException; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.time.Instant; import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; @@ -18,26 +34,18 @@ * This in */ public class ReadOnceInputEvent implements InputEvent { - - private final String appName; - private final String route; - private final String requestUrl; - private final String method; - private final BufferedInputStream body; - private AtomicBoolean consumed = new AtomicBoolean(false); - private QueryParameters queryParameters; + private final AtomicBoolean consumed = new AtomicBoolean(false); private final Headers headers; + private final Instant deadline; + private final String callID; - public ReadOnceInputEvent(String appName, String route, String requestUrl, String method, InputStream body, Headers headers, QueryParameters parameters) { - this.appName = Objects.requireNonNull(appName); - this.route = Objects.requireNonNull(route); - this.requestUrl = Objects.requireNonNull(requestUrl); - this.method = Objects.requireNonNull(method).toUpperCase(); - this.body = new BufferedInputStream(Objects.requireNonNull(body)); - this.headers = Objects.requireNonNull(headers); - this.queryParameters = Objects.requireNonNull(parameters); + public ReadOnceInputEvent(InputStream body, Headers headers, String callID, Instant deadline) { + this.body = new BufferedInputStream(Objects.requireNonNull(body, "body")); + this.headers = Objects.requireNonNull(headers, "headers"); + this.callID = Objects.requireNonNull(callID, "callID"); + this.deadline = Objects.requireNonNull(deadline, "deadline"); body.mark(Integer.MAX_VALUE); } @@ -63,57 +71,22 @@ public T consumeBody(Function dest) { } - /** - * @return The fn application name associated with this call - */ - @Override - public String getAppName() { - return appName; - } - /** - * @return The route associated with this call (starting with a slash) - */ @Override - public String getRoute() { - return route; + public String getCallID() { + return callID; } - /** - * @return The full request URL into the app - */ @Override - public String getRequestUrl() { - return requestUrl; + public Instant getDeadline() { + return deadline; } - /** - * @return The HTTP method (capitalised) of this request - */ - @Override - public String getMethod() { - return method; - } - - /** - * The HTTP headers on the request - * - * @return an immutable map of headers - */ @Override public Headers getHeaders() { return headers; } - /** - * The query parameters of the function invocation - * - * @return an immutable map of query parameters parsed from the request URL - */ - @Override - public QueryParameters getQueryParameters() { - return queryParameters; - } @Override public void close() throws IOException { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ReturnTypeWrapper.java b/runtime/src/main/java/com/fnproject/fn/runtime/ReturnTypeWrapper.java deleted file mode 100644 index 120fffd1..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/ReturnTypeWrapper.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.MethodWrapper; - -/** - * A {@link com.fnproject.fn.api.TypeWrapper} for capturing type information about a method's parameter. - */ -class ReturnTypeWrapper extends MethodTypeWrapper { - - /** - * Constructor - * - * @param method the method which we store return-type information about - */ - ReturnTypeWrapper(MethodWrapper method) { - super(method, resolveType(method.getTargetMethod().getGenericReturnType(), method)); - } -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java index 2f8034b0..88391323 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ByteArrayCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.coercion; import com.fnproject.fn.api.*; @@ -13,7 +29,7 @@ public class ByteArrayCoercion implements InputCoercion, OutputCoercion { public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (method.getReturnType().getParameterClass().equals(byte[].class)) { - return Optional.of(OutputEvent.fromBytes(((byte[]) value), OutputEvent.SUCCESS, "application/octet-stream")); + return Optional.of(OutputEvent.fromBytes(((byte[]) value), OutputEvent.Status.Success, "application/octet-stream")); } else { return Optional.empty(); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ContextCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ContextCoercion.java new file mode 100644 index 00000000..abd80b87 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/ContextCoercion.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.coercion; + +import com.fnproject.fn.api.*; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; +import com.fnproject.fn.api.tracing.TracingContext; +import com.fnproject.fn.runtime.httpgateway.FunctionHTTPGatewayContext; +import com.fnproject.fn.runtime.tracing.OCITracingContext; + +import java.util.Optional; + +/** + * Handles coercion to build in context objects ({@link RuntimeContext}, {@link InvocationContext} , {@link HTTPGatewayContext}) + */ +public class ContextCoercion implements InputCoercion { + + @Override + public Optional tryCoerceParam(InvocationContext currentContext, int arg, InputEvent input, MethodWrapper method) { + Class paramClass = method.getParamType(arg).getParameterClass(); + + if (paramClass.equals(RuntimeContext.class)) { + return Optional.of(currentContext.getRuntimeContext()); + } else if (paramClass.equals(InvocationContext.class)) { + return Optional.of(currentContext); + } else if (paramClass.equals(HTTPGatewayContext.class)) { + return Optional.of(new FunctionHTTPGatewayContext(currentContext)); + } else if (paramClass.equals(TracingContext.class)) { + return Optional.of(new OCITracingContext(currentContext, currentContext.getRuntimeContext())); + } else { + return Optional.empty(); + } + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/InputEventCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/InputEventCoercion.java index 1a04e7e5..fcb4f47b 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/InputEventCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/InputEventCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.coercion; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/OutputEventCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/OutputEventCoercion.java index 93baeb40..fb111f2b 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/OutputEventCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/OutputEventCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.coercion; import com.fnproject.fn.api.InvocationContext; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java index 0fb6c46f..d71d9af9 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/StringCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.coercion; import com.fnproject.fn.api.*; @@ -11,7 +27,7 @@ public class StringCoercion implements InputCoercion, OutputCoercion { @Override public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (method.getReturnType().getParameterClass().equals(String.class)) { - return Optional.of(OutputEvent.fromBytes(((String) value).getBytes(), OutputEvent.SUCCESS, "text/plain")); + return Optional.of(OutputEvent.fromBytes(((String) value).getBytes(), OutputEvent.Status.Success, "text/plain")); } else { return Optional.empty(); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java index 5d0442b6..206dfde5 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/VoidCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.coercion; import com.fnproject.fn.api.InvocationContext; @@ -11,7 +27,7 @@ public class VoidCoercion implements OutputCoercion { @Override public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (method.getReturnType().getParameterClass().equals(Void.class)) { - return Optional.of(OutputEvent.emptyResult(OutputEvent.SUCCESS)); + return Optional.of(OutputEvent.emptyResult(OutputEvent.Status.Success)); } else { return Optional.empty(); } diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java index 93636d38..9688b74f 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/coercion/jackson/JacksonCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.coercion.jackson; import com.fasterxml.jackson.core.JsonProcessingException; @@ -15,10 +31,14 @@ * This supports marshalling and unmarshalling of event parameters and responses to */ public class JacksonCoercion implements InputCoercion, OutputCoercion { - private static String OM_KEY = JacksonCoercion.class.getCanonicalName() + ".om"; + private static final String OM_KEY = JacksonCoercion.class.getCanonicalName() + ".om"; - private static JacksonCoercion instance = new JacksonCoercion(); + private static final JacksonCoercion instance = new JacksonCoercion(); + /** + * Return the global instance of this coercion + * @return a singleton instance of the JSON coercion for the VM + */ public static JacksonCoercion instance() { return instance; } @@ -64,7 +84,7 @@ private static RuntimeException coercionFailed(Type paramType) { public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { try { - return Optional.of(OutputEvent.fromBytes(objectMapper(ctx).writeValueAsBytes(value), OutputEvent.SUCCESS, + return Optional.of(OutputEvent.fromBytes(objectMapper(ctx).writeValueAsBytes(value), OutputEvent.Status.Success, "application/json")); } catch (JsonProcessingException e) { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionClassInstantiationException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionClassInstantiationException.java index 9014c3d6..0ca4a729 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionClassInstantiationException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionClassInstantiationException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.exception; import com.fnproject.fn.api.exception.FunctionLoadException; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionIOException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionIOException.java new file mode 100644 index 00000000..5d982f5b --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionIOException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.exception; + +/** + * The FDK experienced a terminal issue communicating with the platform + */ +public final class FunctionIOException extends RuntimeException { + + + /** + * create a function invocation exception + * + * @param message private message for this exception - + * @param target the underlying user exception that triggered this failure + */ + public FunctionIOException(String message, Throwable target) { + super(message, target); + } + + +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionInitializationException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionInitializationException.java new file mode 100644 index 00000000..f3571353 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/FunctionInitializationException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.exception; + +/** + * The FDK was not able to start up + */ +public final class FunctionInitializationException extends RuntimeException { + + + /** + * create a function invocation exception + * + * @param message private message for this exception - + * @param target the underlying user exception that triggered this failure + */ + public FunctionInitializationException(String message, Throwable target) { + super(message, target); + } + + + public FunctionInitializationException(String message) { + super(message); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java index f12338d2..9c661a3c 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InternalFunctionInvocationException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.exception; import com.fnproject.fn.api.OutputEvent; @@ -19,7 +35,7 @@ public final class InternalFunctionInvocationException extends RuntimeException public InternalFunctionInvocationException(String message, Throwable target) { super(message); this.cause = target; - this.event = OutputEvent.fromBytes(new byte[0], OutputEvent.FAILURE, null); + this.event = OutputEvent.fromBytes(new byte[0], OutputEvent.Status.FunctionError, null); } @@ -43,7 +59,7 @@ public Throwable getCause() { /** * map this exception to an output event - * @return + * @return the output event associated with this exception */ public OutputEvent toOutput() { return event; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java index 8cc01cbc..6bcafdb2 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidEntryPointException.java @@ -1,9 +1,25 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.exception; import com.fnproject.fn.api.exception.FunctionLoadException; /** - * The function entrypoint was malformed. + * The function entry point spec was malformed. */ public class InvalidEntryPointException extends FunctionLoadException { public InvalidEntryPointException(String msg) { diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidFunctionDefinitionException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidFunctionDefinitionException.java index 763666ab..bbc2064e 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidFunctionDefinitionException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/InvalidFunctionDefinitionException.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.exception; import com.fnproject.fn.api.exception.FunctionLoadException; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java b/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java index 7a3249e7..f4da0254 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/exception/PlatformCommunicationException.java @@ -1,5 +1,24 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.exception; +/** + * An error occurred in the + */ public class PlatformCommunicationException extends RuntimeException { public PlatformCommunicationException(String message) { super(message); diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java b/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java deleted file mode 100644 index 21503bf2..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.fnproject.fn.runtime.flow; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class BlobResponse { - @JsonProperty("blob_id") - public String blobId; - - @JsonProperty("length") - public Long blobLength; - - @JsonProperty("content_type") - public String contentType; -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java b/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java deleted file mode 100644 index a002ba93..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/BlobStoreClient.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.fnproject.fn.runtime.flow; - -import java.io.InputStream; -import java.util.function.Function; - -public interface BlobStoreClient { - - - BlobResponse writeBlob(String prefix, byte[] bytes, String contentType); - - T readBlob(String prefix, String blobId, Function reader, String expectedContentType); -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java b/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java deleted file mode 100644 index 752210c1..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/CompleterClientFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.fnproject.fn.runtime.flow; - -import java.io.Serializable; - -public interface CompleterClientFactory extends Serializable { - CompleterClient getCompleterClient(); - - BlobStoreClient getBlobStoreClient(); - -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java b/runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java deleted file mode 100644 index 4b837109..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/EntityReader.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.fnproject.fn.runtime.flow; - -import java.io.InputStream; -import java.util.Map; -import java.util.Optional; - -/** - * Both an HTTP response and an individual part of a multipart MIME stream are constituted of - * a set of headers together with the body stream. This interface abstracts the access to those parts. - */ -interface EntityReader { - String getHeaderElement(String h, String e); - - Optional getHeaderValue(String header); - - InputStream getContentStream(); - - Map getHeaders(); -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java b/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java deleted file mode 100644 index 7561e66b..00000000 --- a/runtime/src/main/java/com/fnproject/fn/runtime/flow/FlowFutureSource.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.fnproject.fn.runtime.flow; - -import com.fnproject.fn.api.flow.FlowFuture; - -/** - * Created on 27/11/2017. - *

- * (c) 2017 Oracle Corporation - */ -public interface FlowFutureSource { - FlowFuture createFlowFuture(CompletionId completionId); -} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContext.java new file mode 100644 index 00000000..32c22611 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContext.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.httpgateway; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.QueryParameters; +import com.fnproject.fn.api.httpgateway.HTTPGatewayContext; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * Created on 19/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FunctionHTTPGatewayContext implements HTTPGatewayContext { + + private final InvocationContext invocationContext; + private final Headers httpRequestHeaders; + private final String method; + private final String requestUrl; + private final QueryParameters queryParameters; + + public FunctionHTTPGatewayContext(InvocationContext invocationContext) { + this.invocationContext = Objects.requireNonNull(invocationContext, "invocationContext"); + + Map> myHeaders = new HashMap<>(); + + String requestUri = ""; + String method = ""; + for (Map.Entry> e : invocationContext.getRequestHeaders().asMap().entrySet()) { + String key = e.getKey(); + if (key.startsWith("Fn-Http-H-")) { + String httpKey = key.substring("Fn-Http-H-".length()); + if (httpKey.length() > 0) { + myHeaders.put(httpKey, e.getValue()); + } + } + + if (key.equals("Fn-Http-Request-Url")) { + requestUri = e.getValue().get(0); + } + if (key.equals("Fn-Http-Method")) { + method = e.getValue().get(0); + } + + } + this.queryParameters = QueryParametersParser.getParams(requestUri); + this.requestUrl = requestUri; + this.method = method; + this.httpRequestHeaders = Headers.emptyHeaders().setHeaders(myHeaders); + + } + + @Override + public InvocationContext getInvocationContext() { + return invocationContext; + } + + @Override + public Headers getHeaders() { + return httpRequestHeaders; + } + + @Override + public String getRequestURL() { + return requestUrl; + } + + @Override + public String getMethod() { + return method; + } + + @Override + public QueryParameters getQueryParameters() { + return queryParameters; + } + + @Override + public void addResponseHeader(String key, String value) { + invocationContext.addResponseHeader("Fn-Http-H-" + key, value); + + } + + public void addResponseHeader(String key, List values) { + if (values == null || values.isEmpty()) { + setResponseHeader(key, null); + } else { + for (String value : values) { + addResponseHeader(key, value); + } + } + } + + @Override + public void setResponseHeader(String key, String value, String... vs) { + + if (Headers.canonicalKey(key).equals(OutputEvent.CONTENT_TYPE_HEADER)) { + invocationContext.setResponseContentType(value); + invocationContext.setResponseHeader("Fn-Http-H-" + key, value); + } else { + invocationContext.setResponseHeader("Fn-Http-H-" + key, value, vs); + + } + + + } + + @Override + public void setStatusCode(int code) { + if (code < 100 || code >= 600) { + throw new IllegalArgumentException("Invalid HTTP status code: " + code); + } + invocationContext.setResponseHeader("Fn-Http-Status", "" + code); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersImpl.java b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersImpl.java new file mode 100644 index 00000000..2f9cfe82 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersImpl.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.httpgateway; + +import com.fnproject.fn.api.QueryParameters; + +import java.io.Serializable; +import java.util.*; + +public class QueryParametersImpl implements QueryParameters, Serializable { + private final Map> params; + + public QueryParametersImpl() { + this.params = new HashMap<>(); + } + + public QueryParametersImpl(Map> params) { + this.params = Objects.requireNonNull(params); + } + + public Optional get(String key) { + Objects.requireNonNull(key); + return Optional.of(getValues(key)) + .filter((values) -> values.size() > 0) + .flatMap((values) -> Optional.ofNullable(values.get(0))); + } + + public List getValues(String key) { + Objects.requireNonNull(key); + List values = this.params.get(key); + if (values == null) { + return Collections.emptyList(); + } + return values; + } + + public int size() { + return params.size(); + } + + @Override + public Map> getAll() { + return new HashMap<>(params); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersParser.java b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersParser.java similarity index 73% rename from runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersParser.java rename to runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersParser.java index 3c1d8f9b..e708d792 100644 --- a/runtime/src/main/java/com/fnproject/fn/runtime/QueryParametersParser.java +++ b/runtime/src/main/java/com/fnproject/fn/runtime/httpgateway/QueryParametersParser.java @@ -1,12 +1,28 @@ -package com.fnproject.fn.runtime; +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.httpgateway; import com.fnproject.fn.api.QueryParameters; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; +import java.util.AbstractMap.SimpleImmutableEntry; import java.util.*; import java.util.Map.Entry; -import java.util.AbstractMap.SimpleImmutableEntry; import java.util.stream.Collectors; import static java.util.stream.Collectors.mapping; diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixServerSocket.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixServerSocket.java new file mode 100644 index 00000000..020bc386 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixServerSocket.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.ntv; + +import java.io.Closeable; +import java.io.IOException; +import java.net.SocketException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixServerSocket implements Closeable { + private final int fd; + private final AtomicBoolean closed = new AtomicBoolean(); + + private UnixServerSocket(int fd) { + this.fd = fd; + } + + + public static UnixServerSocket listen(String fileName, int backlog) throws IOException { + int fd = UnixSocketNative.socket(); + + try { + UnixSocketNative.bind(fd, fileName); + } catch (UnixSocketException e) { + UnixSocketNative.close(fd); + throw e; + } + + + try { + UnixSocketNative.listen(fd, backlog); + } catch (UnixSocketException e) { + UnixSocketNative.close(fd); + throw e; + } + return new UnixServerSocket(fd); + + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false,true)) { + UnixSocketNative.close(fd); + } + } + + public UnixSocket accept(long timeoutMillis) throws IOException { + if (closed.get()) { + throw new SocketException("accept on closed socket"); + } + int newFd = UnixSocketNative.accept(fd, timeoutMillis); + if (newFd == 0) { + return null; + } + return new UnixSocket(newFd); + } + + +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocket.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocket.java new file mode 100644 index 00000000..c1266407 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocket.java @@ -0,0 +1,336 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.ntv; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.*; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This approximates a Java.net.socket for many operations but not by any means all + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public final class UnixSocket extends Socket { + // Fall back to WTF for most unsupported operations + private static final SocketImpl fakeSocketImpl = new SocketImpl() { + @Override + protected void create(boolean stream) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void connect(String host, int port) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void connect(InetAddress address, int port) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void connect(SocketAddress address, int timeout) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void bind(InetAddress host, int port) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void listen(int backlog) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected void accept(SocketImpl s) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + protected InputStream getInputStream() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected OutputStream getOutputStream() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected int available() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected void close() throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + protected void sendUrgentData(int data) throws IOException { + throw new UnsupportedOperationException(); + + } + + @Override + public void setOption(int optID, Object value) throws SocketException { + throw new UnsupportedOperationException(); + + } + + @Override + public Object getOption(int optID) throws SocketException { + throw new UnsupportedOperationException(); + } + }; + + + private final int fd; + private final AtomicBoolean closed = new AtomicBoolean(); + + private final AtomicBoolean inputClosed = new AtomicBoolean(); + private final AtomicBoolean outputClosed = new AtomicBoolean(); + + private final InputStream in; + private final OutputStream out; + + + UnixSocket(int fd) throws SocketException { + super(fakeSocketImpl); + this.fd = fd; + in = new UsInput(); + out = new UsOutput(); + + } + + private class UsInput extends InputStream { + + @Override + public int read() throws IOException { + + byte[] buf = new byte[1]; + int rv = read(buf, 0, 1); + if (rv == -1) { + return -1; + } + return (int) buf[0]; + } + + + public int read(byte b[]) throws IOException { + return this.read(b, 0, b.length); + } + + @Override + public int read(byte b[], int off, int len) throws IOException { + if (inputClosed.get()) { + throw new UnixSocketException("Read on closed stream"); + } + + return UnixSocketNative.recv(fd, b, off, len); + } + + @Override + public void close() throws IOException { + shutdownInput(); + } + } + + + private class UsOutput extends OutputStream { + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + public void write(byte b[], int off, int len) throws IOException { + if (outputClosed.get()) { + throw new UnixSocketException("Write to closed stream"); + } + Objects.requireNonNull(b); + while (len > 0) { + int sent = UnixSocketNative.send(fd, b, off, len); + + if (sent == 0) { + throw new UnixSocketException("No data written to buffer"); + } + off = off + sent; + len = len - sent; + } + } + + @Override + public void close() throws IOException { + shutdownOutput(); + } + } + + + public static UnixSocket connect(String destination) throws IOException { + int fd = UnixSocketNative.socket(); + UnixSocketNative.connect(fd, destination); + return new UnixSocket(fd); + } + + @Override + public InputStream getInputStream() { + return in; + } + + @Override + public OutputStream getOutputStream() { + return out; + } + + + @Override + public synchronized void setReceiveBufferSize(int size) throws SocketException { + UnixSocketNative.setRecvBufSize(fd, size); + } + + @Override + public synchronized void setSendBufferSize(int size) throws SocketException { + UnixSocketNative.setSendBufSize(fd, size); + } + + + @Override + public void setSoTimeout(int timeout) throws SocketException { + UnixSocketNative.setRecvTimeout(fd, timeout); + + } + + @Override + public int getSoTimeout() throws SocketException { + return UnixSocketNative.getRecvTimeout(fd); + } + + @Override + public void connect(SocketAddress endpoint) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public void bind(SocketAddress bindpoint) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public InetAddress getInetAddress() { + return null; + } + + @Override + public InetAddress getLocalAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public int getPort() { + return 0; + } + + @Override + public int getLocalPort() { + return -1; + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return null; + } + + @Override + public SocketAddress getLocalSocketAddress() { + return null; + } + + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean isBound() { + return true; + } + + + @Override + public boolean isClosed() { + return closed.get(); + } + + + @Override + public boolean isInputShutdown() { + return inputClosed.get(); + } + + @Override + public boolean isOutputShutdown() { + return outputClosed.get(); + } + + @Override + public void shutdownInput() throws IOException { + if (inputClosed.compareAndSet(false, true)) { + UnixSocketNative.shutdown(fd, true, false); + } else { + throw new SocketException("Input already shut down"); + } + } + + @Override + public void shutdownOutput() throws IOException { + if (outputClosed.compareAndSet(false, true)) { + UnixSocketNative.shutdown(fd, false, true); + } else { + throw new SocketException("Output already shut down"); + } + } + + @Override + public void close() throws IOException { + if (closed.compareAndSet(false, true)) { + UnixSocketNative.close(fd); + } + } + + +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketException.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketException.java new file mode 100644 index 00000000..e30707d0 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.ntv; + +import java.net.SocketException; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixSocketException extends SocketException { + public UnixSocketException(String message, String detail) { + super(message + ":" + detail); + } + + public UnixSocketException(String message) { + super(message); + } +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketNative.java b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketNative.java new file mode 100644 index 00000000..2891f08b --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/ntv/UnixSocketNative.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.ntv; + + +import java.io.IOException; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +class UnixSocketNative { + + public UnixSocketNative() {} + + static { + String lib = System.mapLibraryName("fnunixsocket"); + + + String libLocation = System.getProperty("com.fnproject.java.native.libdir"); + if (libLocation != null) { + if (!libLocation.endsWith("/")) { + libLocation = libLocation + "/"; + } + lib = libLocation + lib; + System.load(lib); + }else{ + System.loadLibrary("fnunixsocket"); + } + } + + public static native int socket() throws IOException; + + public static native void bind(int socket, String path) throws UnixSocketException; + + public static native void connect(int socket, String path) throws IOException; + + public static native void listen(int socket, int backlog) throws UnixSocketException; + + public static native int accept(int socket, long timeoutMs) throws IOException; + + public static native int recv(int socket, byte[] buffer, int offset, int length) throws IOException; + + public static native int send(int socket, byte[] buffer, int offset, int length) throws IOException; + + public static native void close(int socket) throws UnixSocketException; + + public static native void setSendTimeout(int socket, int timeout) throws UnixSocketException; + + public static native int getSendTimeout(int socket) throws IOException; + + public static native void setRecvTimeout(int socket, int timeout) throws UnixSocketException; + + public static native int getRecvTimeout(int socket) throws UnixSocketException; + + public static native void setSendBufSize(int socket, int bufSize) throws UnixSocketException; + + public static native void setRecvBufSize(int socket, int bufSize) throws UnixSocketException; + + public static native void shutdown(int socket, boolean input, boolean output) throws UnixSocketException; +} diff --git a/runtime/src/main/java/com/fnproject/fn/runtime/tracing/OCITracingContext.java b/runtime/src/main/java/com/fnproject/fn/runtime/tracing/OCITracingContext.java new file mode 100644 index 00000000..d4bf5226 --- /dev/null +++ b/runtime/src/main/java/com/fnproject/fn/runtime/tracing/OCITracingContext.java @@ -0,0 +1,131 @@ +package com.fnproject.fn.runtime.tracing; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.RuntimeContext; +import com.fnproject.fn.api.tracing.TracingContext; + +public class OCITracingContext implements TracingContext { + private final InvocationContext invocationContext; + private final RuntimeContext runtimeContext; + private String traceCollectorURL; + private String traceId; + private String spanId; + private String parentSpanId; + private Boolean sampled = true; + private String flags; + private Boolean tracingEnabled; + private String appName; + private String fnName; + private static final String PLACEHOLDER_TRACE_COLLECTOR_URL = "http://localhost:9411/api/v2/span"; + private static final String PLACEHOLDER_TRACE_ID = "1"; + + + public OCITracingContext(InvocationContext invocationContext, RuntimeContext runtimeContext) { + this.invocationContext = invocationContext; + this.runtimeContext = runtimeContext; + + configureDefaultValue(); + configure(runtimeContext); + + if(tracingEnabled) + configure(invocationContext.getRequestHeaders()); + } + + private void configureDefaultValue() { + this.traceCollectorURL = PLACEHOLDER_TRACE_COLLECTOR_URL; + this.traceId = PLACEHOLDER_TRACE_ID; + this.spanId = PLACEHOLDER_TRACE_ID; + this.parentSpanId = PLACEHOLDER_TRACE_ID; + } + + private void configure(RuntimeContext runtimeContext) { + if(runtimeContext != null && runtimeContext.getConfigurationByKey("OCI_TRACE_COLLECTOR_URL").get() != null + && runtimeContext.getConfigurationByKey("OCI_TRACING_ENABLED").get() != null) { + this.traceCollectorURL = + runtimeContext.getConfigurationByKey("OCI_TRACE_COLLECTOR_URL").get().isEmpty() + ?PLACEHOLDER_TRACE_COLLECTOR_URL + :runtimeContext.getConfigurationByKey("OCI_TRACE_COLLECTOR_URL").get(); + try { + Integer tracingEnabledAsInt = Integer.parseInt(runtimeContext.getConfigurationByKey("OCI_TRACING_ENABLED").get()); + this.tracingEnabled = tracingEnabledAsInt != 0; + } catch(java.lang.NumberFormatException ex) { + this.tracingEnabled = false; + } + this.appName = runtimeContext.getAppName(); + this.fnName = runtimeContext.getFunctionName(); + } + } + + private void configure(Headers headers) { + this.flags = headers.get("x-b3-flags").orElse(""); + if (headers.get("x-b3-sampled").isPresent() && Integer.parseInt(headers.get("x-b3-sampled").get()) == 0) { + this.sampled = false; + return; + } + this.sampled = true; + this.traceId = headers.get("x-b3-traceid").orElse(PLACEHOLDER_TRACE_ID); + this.spanId = headers.get("x-b3-spanid").orElse(PLACEHOLDER_TRACE_ID); + this.parentSpanId = headers.get("x-b3-parentspanid").orElse(PLACEHOLDER_TRACE_ID); + } + + @Override + public InvocationContext getInvocationContext() { + return invocationContext; + } + + @Override + public RuntimeContext getRuntimeContext() { + return runtimeContext; + } + + @Override + public String getServiceName() { + return this.appName.toLowerCase() + "::" + this.fnName.toLowerCase(); + } + + @Override + public String getTraceCollectorURL() { + return traceCollectorURL; + } + + @Override + public String getTraceId() { + return traceId; + } + + @Override + public String getSpanId() { + return spanId; + } + + @Override + public String getParentSpanId() { + return parentSpanId; + } + + @Override + public Boolean isSampled() { + return sampled; + } + + @Override + public String getFlags() { + return flags; + } + + @Override + public Boolean isTracingEnabled() { + return tracingEnabled; + } + + @Override + public String getAppName() { + return appName; + } + + @Override + public String getFunctionName() { + return fnName; + } +} diff --git a/runtime/src/main/resources/META-INF/native-image/com.fnproject.fn/runtime/jni-config.json b/runtime/src/main/resources/META-INF/native-image/com.fnproject.fn/runtime/jni-config.json new file mode 100644 index 00000000..05def46f --- /dev/null +++ b/runtime/src/main/resources/META-INF/native-image/com.fnproject.fn/runtime/jni-config.json @@ -0,0 +1,22 @@ +[ + { + "name" : "com.fnproject.fn.runtime.ntv.UnixSocketNative", + "methods" : [ + { "name" : "socket" }, + { "name" : "bind" }, + { "name" : "connect" }, + { "name" : "listen" }, + { "name" : "accept" }, + { "name" : "recv" }, + { "name" : "send" }, + { "name" : "close" }, + { "name" : "setSendTimeout" }, + { "name" : "getSendTimeout" }, + { "name" : "setRecvTimeout" }, + { "name" : "getRecvTimeout" }, + { "name" : "setSendBufSize" }, + { "name" : "setRecvBufSize" }, + { "name" : "shutdown"} + ] + } +] diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java index cdde6e98..bc7a6e1e 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ConfigurationMethodsTest.java @@ -1,5 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; +import com.fnproject.fn.api.OutputEvent; import com.fnproject.fn.runtime.testfns.TestFnWithConfigurationMethods; import org.junit.Rule; import org.junit.Test; @@ -16,51 +33,51 @@ public class ConfigurationMethodsTest { @Test public void staticTargetWithNoConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.StaticTargetNoConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("StaticTargetNoConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("StaticTargetNoConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithNoConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetNoConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetNoConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetNoConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void staticTargetWithStaticConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.StaticTargetStaticConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("StaticTargetStaticConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("StaticTargetStaticConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithStaticConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetStaticConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetStaticConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetStaticConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void staticTargetWithInstanceConfigurationIsAnError() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); String expectedMessage = "Configuration method " + "'config'" + @@ -68,78 +85,78 @@ public void staticTargetWithInstanceConfigurationIsAnError() throws Exception { fn.thenRun(TestFnWithConfigurationMethods.StaticTargetInstanceConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEmpty(); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).startsWith(expectedMessage); assertThat(fn.exitStatus()).isEqualTo(2); } @Test public void instanceTargetWithInstanceConfigurationIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetInstanceConfiguration.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetInstanceConfiguration\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetInstanceConfiguration\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void staticTargetWithStaticConfigurationWithoutRuntimeContextParameterIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.StaticTargetStaticConfigurationNoRuntime.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("StaticTargetStaticConfigurationNoRuntime\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("StaticTargetStaticConfigurationNoRuntime\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithStaticConfigurationWithoutRuntimeContextParameterIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetStaticConfigurationNoRuntime.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetStaticConfigurationNoRuntime\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetStaticConfigurationNoRuntime\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void instanceTargetWithInstanceConfigurationWithoutRuntimeContextParameterIsOK() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.InstanceTargetInstanceConfigurationNoRuntime.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("InstanceTargetInstanceConfigurationNoRuntime\nHello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("InstanceTargetInstanceConfigurationNoRuntime\nHello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldReturnDefaultParameterIfNotProvided() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnWithConfigurationMethods.WithGetConfigurationByKey.class, "getParam"); - assertThat(fn.getStdOutAsString()).isEqualTo("default"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("default"); } @Test - public void shouldReturnSetConfigParameterWhenProvided() { + public void shouldReturnSetConfigParameterWhenProvided() { String value = "value"; fn.setConfig("PARAM", value); - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnWithConfigurationMethods.WithGetConfigurationByKey.class, "getParam"); - assertThat(fn.getStdOutAsString()).isEqualTo(value); + assertThat(fn.getOnlyOutputAsString()).isEqualTo(value); } @Test public void nonVoidConfigurationMethodIsAnError() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodIsNonVoid.class, "echo"); @@ -147,7 +164,9 @@ public void nonVoidConfigurationMethodIsAnError() throws Exception { "'config'" + " does not have a void return type"; - assertThat(fn.getStdOutAsString()).isEmpty(); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> { + assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError); + }); assertThat(fn.getStdErrAsString()).startsWith(expectedMessage); assertThat(fn.exitStatus()).isEqualTo(2); } @@ -156,13 +175,13 @@ public void nonVoidConfigurationMethodIsAnError() throws Exception { @Test public void shouldBeAbleToAccessConfigInConfigurationMethodWhenDefault() { fn.setConfig("FOO", "BAR"); - fn.givenDefaultEvent() + fn.givenEvent() .withBody("FOO") .enqueue(); fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).isEqualTo("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test @@ -174,7 +193,7 @@ public void shouldBeAbleToAccessConfigInConfigurationMethodWhenHttp() { fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).contains("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).contains("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test @@ -186,19 +205,19 @@ public void shouldOnlyExtractConfigFromEnvironmentNotHeaderWhenHttp() { fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).doesNotContain("BAR"); + assertThat(fn.getOnlyOutputAsString()).doesNotContain("BAR"); } @Test public void shouldNotBeAbleToAccessHeadersInConfigurationWhenDefault() { - fn.givenDefaultEvent() + fn.givenEvent() .withHeader("FOO", "BAR") .withBody("HEADER_FOO") .enqueue(); fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test @@ -210,18 +229,18 @@ public void shouldNotBeAbleToAccessHeadersInConfigurationWhenHttp() { fn.thenRun(TestFnWithConfigurationMethods.ConfigurationMethodWithAccessToConfig.class, "configByKey"); - assertThat(fn.getStdOutAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); + assertThat(fn.getOnlyOutputAsString()).doesNotContain("ConfigurationMethodWithAccessToConfig\nBAR"); } @Test public void shouldCallInheritedConfigMethodsInRightOrder() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); TestFnWithConfigurationMethods.SubConfigClass.order = ""; fn.thenRun(TestFnWithConfigurationMethods.SubConfigClass.class, "invoke"); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); assertThat(TestFnWithConfigurationMethods.SubConfigClass.order) .matches("\\.baseStatic1\\.subStatic1\\.baseFn\\d\\.baseFn\\d\\.subFn\\d\\.subFn\\d\\.subFn\\d\\.subFn\\d"); } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java index 559af89d..47c3398b 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/DataBindingTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.runtime.testfns.*; @@ -16,132 +32,132 @@ public class DataBindingTest { @Test public void shouldUseInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithConfig.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseInputCoercionSpecifiedWithAnnotation() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithAnnotation.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithConfig.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseOutputCoercionSpecifiedWithAnnotation() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithAnnotation.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseFirstInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithMultipleCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("HELLO WORLD"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("HELLO WORLD"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseFirstOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithMultipleCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("HELLO WORLD"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("HELLO WORLD"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseBuiltInInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); - fn.thenRun(CustomDataBindingFnWithNoUserCoersions.class, "echo"); + fn.thenRun(CustomDataBindingFnWithNoUserCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("Hello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseBuiltInOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithNoUserCoercions.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("Hello World"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello World"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseSecondInputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithDudCoercion.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldUseSecondOutputCoercionSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomOutputDataBindingFnWithDudCoercion.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("dlroW olleH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("dlroW olleH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldApplyCoercionsForInputAndOutputSpecifiedOnFunctionRuntimeContext() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnInputOutput.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("DLROW OLLEH"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("DLROW OLLEH"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @Test public void shouldPrioritiseAnnotationOverConfig() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(CustomDataBindingFnWithAnnotationAndConfig.class, "echo"); - assertThat(fn.getStdOutAsString()).isEqualTo("HELLO WORLD"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("HELLO WORLD"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/DefaultEventCodecTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/DefaultEventCodecTest.java deleted file mode 100644 index d7a501a7..00000000 --- a/runtime/src/test/java/com/fnproject/fn/runtime/DefaultEventCodecTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import org.apache.commons.io.input.NullInputStream; -import org.apache.commons.io.output.NullOutputStream; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; - -import static com.fnproject.fn.runtime.HeaderBuilder.headerEntry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.Assert.fail; - -public class DefaultEventCodecTest { - - private final Map emptyConfig = new HashMap<>(); - private InputStream asStream(String s) { - return new ByteArrayInputStream(s.getBytes()); - } - - @Test - public void shouldExtractBasicEvent() { - Map env = new HashMap<>(); - env.put("FN_FORMAT", "default"); - env.put("FN_METHOD", "GET"); - env.put("FN_APP_NAME", "testapp"); - env.put("FN_PATH", "/route"); - env.put("FN_REQUEST_URL", "http://test.com/fn/tryInvoke"); - - env.put("FN_HEADER_CONTENT_TYPE", "text/plain"); - env.put("FN_HEADER_ACCEPT", "text/html, text/plain;q=0.9"); - env.put("FN_HEADER_ACCEPT_ENCODING", "gzip"); - env.put("FN_HEADER_USER_AGENT", "userAgent"); - - Map config = new HashMap<>(); - config.put("configparam", "configval"); - config.put("CONFIGPARAM", "CONFIGVAL"); - - DefaultEventCodec codec = new DefaultEventCodec(env, asStream("input"), new NullOutputStream()); - InputEvent evt = codec.readEvent().get(); - assertThat(evt.getMethod()).isEqualTo("GET"); - assertThat(evt.getAppName()).isEqualTo("testapp"); - assertThat(evt.getRoute()).isEqualTo("/route"); - assertThat(evt.getRequestUrl()).isEqualTo("http://test.com/fn/tryInvoke"); - - - assertThat(evt.getHeaders().getAll().size()).isEqualTo(4); - assertThat(evt.getHeaders().getAll()).contains( - headerEntry("CONTENT_TYPE", "text/plain"), - headerEntry("ACCEPT_ENCODING", "gzip"), - headerEntry("ACCEPT", "text/html, text/plain;q=0.9"), - headerEntry("USER_AGENT", "userAgent")); - - evt.consumeBody((body) -> assertThat(body).hasSameContentAs(asStream("input"))); - - assertThat(codec.shouldContinue()).isFalse(); - } - - - - @Test - public void shouldRejectMissingEnv() { - Map requiredEnv = new HashMap<>(); - - requiredEnv.put("FN_PATH", "/route"); - requiredEnv.put("FN_METHOD", "GET"); - requiredEnv.put("FN_APP_NAME", "app_name"); - requiredEnv.put("FN_REQUEST_URL", "http://test.com/fn/tryInvoke"); - - for (String key : requiredEnv.keySet()) { - Map newEnv = new HashMap<>(requiredEnv); - newEnv.remove(key); - - DefaultEventCodec codec = new DefaultEventCodec(newEnv, asStream("input"), new NullOutputStream()); - - try{ - codec.readEvent(); - fail("Should have rejected missing env "+ key); - }catch(FunctionInputHandlingException e){ - assertThat(e).hasMessageContaining("Required environment variable " + key+ " is not set - are you running a function outside of fn run?"); - } - } - - } - - @Test - public void shouldWriteOutputDirectlyToOutputStream() throws IOException{ - - OutputEvent evt = OutputEvent.fromBytes("hello".getBytes(),OutputEvent.SUCCESS,"text/plain"); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - DefaultEventCodec codec = new DefaultEventCodec(new HashMap<>(), new NullInputStream(0),bos); - codec.writeEvent(evt); - assertThat(new String(bos.toByteArray())).isEqualTo("hello"); - - } -} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java index 1a019b47..95162fbe 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/EndToEndInvokeTest.java @@ -1,14 +1,32 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; +import java.util.Arrays; +import java.util.List; + +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; import com.fnproject.fn.runtime.testfns.BadTestFnDuplicateMethods; import com.fnproject.fn.runtime.testfns.TestFn; import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; -import java.util.Arrays; -import java.util.List; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -27,13 +45,13 @@ public static void setup() { @Test public void shouldResolveTestCallWithEnvVarParams() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); TestFn.setOutput("Hello World Out"); fn.thenRun(TestFn.class, "fnStringInOut"); assertThat(TestFn.getInput()).isEqualTo("Hello World"); - assertThat(fn.getStdOutAsString()).isEqualTo("Hello World Out"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello World Out"); assertThat(fn.getStdErrAsString()).isEmpty(); assertThat(fn.exitStatus()).isZero(); } @@ -50,85 +68,81 @@ public void shouldResolveTestCallFromHotCall() throws Exception { @Test public void shouldSerializeGenericCollections() throws Exception { - fn.givenDefaultEvent().withBody("four").enqueue(); + fn.givenEvent().withBody("four").enqueue(); fn.thenRun(TestFn.class, "fnGenericCollections"); - assertThat(fn.getStdOutAsString()).isEqualTo("[\"one\",\"two\",\"three\",\"four\"]"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("[\"one\",\"two\",\"three\",\"four\"]"); } @Test public void shouldSerializeAnimalCollections() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "fnGenericAnimal"); - assertThat(fn.getStdOutAsString()).isEqualTo("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]"); } @Test public void shouldDeserializeGenericCollections() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("[\"one\",\"two\",\"three\",\"four\"]") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("[\"one\",\"two\",\"three\",\"four\"]") + .enqueue(); fn.thenRun(TestFn.class, "fnGenericCollectionsInput"); - assertThat(fn.getStdOutAsString()).isEqualTo("ONE"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("ONE"); } @Test public void shouldDeserializeCustomObjects() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]") + .enqueue(); fn.thenRun(TestFn.class, "fnCustomObjectsCollectionsInput"); - assertThat(fn.getStdOutAsString()).isEqualTo("Spot"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Spot"); } @Test public void shouldDeserializeComplexCustomObjects() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("{\"number1\":[{\"name\":\"Spot\",\"age\":6}]," + - "\"number2\":[{\"name\":\"Spot\",\"age\":16}]}") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("{\"number1\":[{\"name\":\"Spot\",\"age\":6}]," + + "\"number2\":[{\"name\":\"Spot\",\"age\":16}]}") + .enqueue(); fn.thenRun(TestFn.class, "fnCustomObjectsNestedCollectionsInput"); - assertThat(fn.getStdOutAsString()).isEqualTo("Spot"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Spot"); } @Test public void shouldHandledStreamedHotInputEvent() throws Exception { fn.givenEvent() - .withBody("message1") - .withMethod("POST") - .enqueue(); + .withBody("message1") + .enqueue(); fn.givenEvent() - .withBody("message2") - .withMethod("GET") - .enqueue(); + .withBody("message2") + .enqueue(); - fn.thenRun(TestFn.class,"fnEcho"); + fn.thenRun(TestFn.class, "fnEcho"); - List responses = fn.getParsedHttpResponses(); + List responses = fn.getOutputs(); assertThat(responses).size().isEqualTo(2); - FnTestHarness.ParsedHttpResponse r1 = responses.get(0); - assertThat(r1.getBodyAsString()).isEqualTo("message1"); - - FnTestHarness.ParsedHttpResponse r2 = responses.get(1); - assertThat(r2.getBodyAsString()).isEqualTo("message2"); - + FnTestHarness.TestOutput r1 = responses.get(0); + assertThat(new String(r1.getBody())).isEqualTo("message1"); + FnTestHarness.TestOutput r2 = responses.get(1); + assertThat(new String(r2.getBody())).isEqualTo("message2"); } @@ -136,22 +150,22 @@ public void shouldHandledStreamedHotInputEvent() throws Exception { @Test public void shouldPrintErrorOnUnknownMethod() throws Exception { - - + fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "unknownMethod"); - assertThat(fn.getStdOutAsString()).isEqualTo(""); + assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).startsWith("Method 'unknownMethod' was not found in class 'com.fnproject.fn.runtime.testfns.TestFn'"); - } @Test public void shouldPrintErrorOnUnknownClass() throws Exception { + fn.givenEvent().enqueue(); fn.thenRun("com.fnproject.unknown.Class", "unknownMethod"); - - assertThat(fn.getStdOutAsString()).isEqualTo(""); + assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).startsWith("Class 'com.fnproject.unknown.Class' not found in function jar"); } @@ -159,32 +173,32 @@ public void shouldPrintErrorOnUnknownClass() throws Exception { @Test public void shouldDirectStdOutToStdErrForFunctions() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "fnWritesToStdout"); - assertThat(fn.getStdOutAsString()).isEqualTo(""); + assertThat(fn.getOnlyOutputAsString()).isEqualTo(""); assertThat(fn.getStdErrAsString()).isEqualTo("STDOUT"); } @Test public void shouldTerminateDefaultContainerOnExceptionWithError() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFn.class, "fnThrowsException"); assertThat(fn.getStdErrAsString()).startsWith("An error occurred in function:"); - assertThat(fn.getStdOutAsString()).isEmpty(); + assertThat(fn.getOnlyOutputAsString()).isEmpty(); assertThat(fn.exitStatus()).isEqualTo(1); } @Test public void shouldReadJsonObject() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("{\"foo\":\"bar\"}") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("{\"foo\":\"bar\"}") + .enqueue(); fn.thenRun(TestFn.class, "fnReadsJsonObj"); @@ -194,21 +208,21 @@ public void shouldReadJsonObject() throws Exception { @Test public void shouldWriteJsonData() throws Exception { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); TestFn.JsonData data = new TestFn.JsonData(); data.foo = "bar"; TestFn.setOutput(data); fn.thenRun(TestFn.class, "fnWritesJsonObj"); - assertThat(fn.getStdOutAsString()).isEqualTo("{\"foo\":\"bar\"}"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("{\"foo\":\"bar\"}"); } @Test public void shouldReadBytesOnDefaultCodec() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); fn.thenRun(TestFn.class, "fnReadsBytes"); @@ -216,34 +230,48 @@ public void shouldReadBytesOnDefaultCodec() throws Exception { } + @Test + public void shouldPrintLogFrame() throws Exception { + fn.setConfig("FN_LOGFRAME_NAME", "containerID"); + fn.setConfig("FN_LOGFRAME_HDR", "fnID"); + fn.givenEvent().withHeader("fnID", "fnIDVal").withBody( "Hello world!").enqueue(); + + fn.thenRun(TestFn.class, "fnEcho"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("Hello world!"); + // stdout gets redirected to stderr - hence printing out twice + assertThat(fn.getStdErrAsString()).isEqualTo("\ncontainerID=fnIDVal\n\n\ncontainerID=fnIDVal\n\n"); + + } + @Test public void shouldWriteBytesOnDefaultCodec() throws Exception { - fn.givenDefaultEvent().withBody("Hello World").enqueue(); + fn.givenEvent().withBody("Hello World").enqueue(); TestFn.setOutput("OK".getBytes()); fn.thenRun(TestFn.class, "fnWritesBytes"); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @Test public void shouldRejectDuplicateMethodsInFunctionClass() throws Exception { + fn.givenEvent().enqueue(); - fn.thenRun(BadTestFnDuplicateMethods.class,"fn"); - - assertThat(fn.getStdOutAsString()).isEmpty(); + fn.thenRun(BadTestFnDuplicateMethods.class, "fn"); + assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).startsWith("Multiple methods match"); } @Test public void shouldReadRawJson() throws Exception { - fn.givenDefaultEvent() - .withHeader("Content-type", "application/json") - .withBody("[\"foo\",\"bar\"]") - .enqueue(); + fn.givenEvent() + .withHeader("Content-type", "application/json") + .withBody("[\"foo\",\"bar\"]") + .enqueue(); fn.thenRun(TestFn.class, "fnReadsRawJson"); @@ -253,24 +281,35 @@ public void shouldReadRawJson() throws Exception { } - @Test - public void shouldReadMultipleMessageWhenInputIsNotParsed() throws Exception { - fn.givenHttpEvent().withBody("Hello World 1").enqueue(); - fn.givenHttpEvent().withBody("Hello World 2").enqueue(); + public void shouldReadInputHeaders() throws Exception{ + fn.givenEvent() + .withHeader("myHeader", "Foo") + .withHeader("a-n-header", "b0o","b10") + .enqueue(); + fn.thenRun(TestFn.class, "readRawEvent"); + InputEvent iev = (InputEvent)TestFn.getInput(); + assertThat(iev).isNotNull(); + assertThat(iev.getHeaders().getAllValues("Myheader")).contains("Foo"); + assertThat(iev.getHeaders().getAllValues("A-N-Header")).contains("b0o","b10"); - fn.thenRun(TestFn.class, "readSecondInput"); - List results = fn.getParsedHttpResponses(); - assertThat(results).hasSize(2); - assertThat(results.get(0).getStatus()).isEqualTo(200); - assertThat(results.get(0).getBodyAsString()).isEqualTo("first;"); - assertThat(results.get(1).getStatus()).isEqualTo(200); - assertThat(results.get(1).getBodyAsString()).isEqualTo("Hello World 2"); + } + @Test + public void shouldExposeOutputHeaders() throws Exception{ + fn.givenEvent() + .enqueue(); + fn.thenRun(TestFn.class, "setsOutputHeaders"); - } + FnTestHarness.TestOutput to = fn.getOutputs().get(0); + System.err.println("got response" + to ); + assertThat(to.getContentType()).contains("foo-ct"); + assertThat(to.getHeaders().get("Header-1")).contains("v1"); + assertThat(to.getHeaders().getAllValues("A")).contains("b1","b2"); + + } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java index f4f96c07..b24125da 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ErrorMessagesTest.java @@ -1,11 +1,29 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; +import com.fnproject.fn.api.OutputEvent; import com.fnproject.fn.runtime.testfns.ErrorMessages; import not.in.com.fnproject.fn.StacktraceFilteringTestFunctions; +import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class ErrorMessagesTest { @@ -14,12 +32,14 @@ public class ErrorMessagesTest { private void assertIsErrorWithoutStacktrace(String errorMessage) { assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains(errorMessage); assertThat(fn.getStdErrAsString().split(System.getProperty("line.separator")).length).isEqualTo(1); } - private void assertIsEntrypointErrorWithStacktrace(String errorMessage) { + private void assertIsEntryPointErrorWithStacktrace(String errorMessage) { assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains(errorMessage); assertThat(fn.getStdErrAsString().split(System.getProperty("line.separator")).length).isGreaterThan(1); assertThat(fn.getStdErrAsString()).doesNotContain("at com.fnproject.fn.runtime"); @@ -36,50 +56,53 @@ private void assertIsFunctionErrorWithStacktrace(String errorMessage) { @Test public void userSpecifiesNonExistentClass(){ + fn.givenEvent().enqueue(); fn.thenRun("NonExistentClass", "method"); assertIsErrorWithoutStacktrace("Class 'NonExistentClass' not found in function jar. It's likely that the 'cmd' entry in func.yaml is incorrect."); } @Test public void userSpecifiesClassWithNoMethods(){ + fn.givenEvent().enqueue(); fn.thenRun(ErrorMessages.NoMethodsClass.class, "thisClassHasNoMethods"); assertIsErrorWithoutStacktrace("Method 'thisClassHasNoMethods' was not found in class 'com.fnproject.fn.runtime.testfns.ErrorMessages.NoMethodsClass'. Available functions were: []"); } @Test public void userSpecifiesMethodWhichDoesNotExist(){ + fn.givenEvent().enqueue(); fn.thenRun(ErrorMessages.OneMethodClass.class, "notTheMethod"); assertIsErrorWithoutStacktrace("Method 'notTheMethod' was not found in class 'com.fnproject.fn.runtime.testfns.ErrorMessages.OneMethodClass'. Available functions were: [theMethod]"); } @Test public void userFunctionInputCoercionError(){ - fn.givenDefaultEvent().withBody("This is not a...").enqueue(); + fn.givenEvent().withBody("This is not a...").enqueue(); fn.thenRun(ErrorMessages.OtherMethodsClass.class, "takesAnInteger"); - assertIsEntrypointErrorWithStacktrace("An exception was thrown during Input Coercion: Failed to coerce event to user function parameter type class java.lang.Integer"); + assertIsEntryPointErrorWithStacktrace("An exception was thrown during Input Coercion: Failed to coerce event to user function parameter type class java.lang.Integer"); } @Test public void objectConstructionThrowsARuntimeException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.ExceptionInConstructor.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Whoops"); + assertIsEntryPointErrorWithStacktrace("Whoops"); } @Test public void objectConstructionThrowsADeepException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.DeepExceptionInConstructor.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Inside a method called by the constructor"); + assertIsEntryPointErrorWithStacktrace("Inside a method called by the constructor"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConstructor.naughtyMethod"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConstructor."); } @Test public void objectConstructionThrowsANestedException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.NestedExceptionInConstructor.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Oh no!"); + assertIsEntryPointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Oh no!"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$NestedExceptionInConstructor.naughtyMethod"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.ArithmeticException: / by zero"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$NestedExceptionInConstructor.naughtyMethod"); @@ -88,25 +111,25 @@ public void objectConstructionThrowsANestedException(){ @Test public void fnConfigurationThrowsARuntimeException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.ExceptionInConfiguration.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Config fail"); + assertIsEntryPointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Config fail"); } @Test public void fnConfigurationThrowsADeepException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.DeepExceptionInConfiguration.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Deep config fail"); + assertIsEntryPointErrorWithStacktrace("Caused by: java.lang.RuntimeException: Deep config fail"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConfiguration.throwDeep"); assertThat(fn.getStdErrAsString()).contains("at not.in.com.fnproject.fn.StacktraceFilteringTestFunctions$DeepExceptionInConfiguration.config"); } @Test public void fnConfigurationThrowsANestedException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.NestedExceptionInConfiguration.class, "invoke"); - assertIsEntrypointErrorWithStacktrace("Error invoking configuration method: config"); + assertIsEntryPointErrorWithStacktrace("Error invoking configuration method: config"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: nested at 3"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: nested at 2"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: nested at 1"); @@ -116,7 +139,7 @@ public void fnConfigurationThrowsANestedException(){ @Test public void functionThrowsNestedException(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(StacktraceFilteringTestFunctions.CauseStackTraceInResult.class, "invoke"); assertIsFunctionErrorWithStacktrace("An error occurred in function: Throw two"); assertThat(fn.getStdErrAsString()).contains("Caused by: java.lang.RuntimeException: Throw two"); diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java b/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java index 5434029e..b9d06ad3 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/FnTestHarness.java @@ -1,30 +1,42 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; -import org.apache.commons.io.IOUtils; +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; import org.apache.commons.io.output.TeeOutputStream; -import org.apache.http.HttpResponse; -import org.apache.http.NoHttpResponseException; -import org.apache.http.impl.io.ContentLengthInputStream; -import org.apache.http.impl.io.DefaultHttpResponseParser; -import org.apache.http.impl.io.HttpTransportMetricsImpl; -import org.apache.http.impl.io.SessionInputBufferImpl; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; import java.io.*; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.*; /** - * Function testing harness - this provides the call-side of iron functions' process contract for both HTTP and default type functions + * Function internal testing harness - this provides access the call-side of the functions contract excluding the codec which is mocked */ public class FnTestHarness implements TestRule { - private Map vars = new HashMap<>(); - private boolean hasEvents = false; - private InputStream pendingInput = new ByteArrayInputStream(new byte[0]); - private ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); - private ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + private final List pendingInput = Collections.synchronizedList(new ArrayList<>()); + private final List output = Collections.synchronizedList(new ArrayList<>()); + private final ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); private int exitStatus = -1; private final Map config = new HashMap<>(); @@ -46,44 +58,55 @@ public void setConfig(String key, String value) { /** * Gets a function config variable by key, or null if absent * - * @param key the configuration key + * @param key the configuration key */ public String getConfig(String key) { return config.get(key.toUpperCase().replaceAll("[- ]", "_")); } + public String getOnlyOutputAsString() { + if (output.size() != 1) { + throw new IllegalStateException("expecting exactly one result, got " + output.size()); + } + return new String(output.get(0).getBody()); + } + /** * Builds a mocked input event into the function runtime */ - public abstract class EventBuilder { - protected String method = "GET"; - protected String appName = "appName"; - protected String route = "/route"; - protected String requestUrl = "http://example.com/r/appName/route"; - protected InputStream body = new ByteArrayInputStream(new byte[0]); - protected int contentLength = 0; - protected String contentType = null; - - protected Map headers = new HashMap<>(); + public final class EventBuilder { + InputStream body = new ByteArrayInputStream(new byte[0]); + String contentType = null; + String callID = "callID"; + Instant deadline = Instant.now().plus(1, ChronoUnit.HOURS); + Headers headers = Headers.emptyHeaders(); /** * Add a header to the input * Duplicate headers will be overwritten * * @param key header key - * @param value header value + * @param v1 header value + * @param vs other header values */ - public EventBuilder withHeader(String key, String value) { + public EventBuilder withHeader(String key, String v1, String... vs) { Objects.requireNonNull(key, "key"); - Objects.requireNonNull(value, "value"); - headers.put(key, value); + Objects.requireNonNull(v1, "value"); + Objects.requireNonNull(vs, "vs"); + Arrays.stream(vs).forEach(v -> Objects.requireNonNull(v, "null value in varags list ")); + headers = headers.addHeader(key, v1); + for (String v : vs) { + headers = headers.addHeader(key, v); + } + return this; } /** * Add a series of headers to the input * This may override duplicate headers - * @param headers Map of headers to add + * + * @param headers Map of headers to add */ public EventBuilder withHeaders(Map headers) { headers.forEach(this::withHeader); @@ -94,21 +117,11 @@ public EventBuilder withHeaders(Map headers) { * Set the body of the request by providing an InputStream * * @param body the bytes of the body - * @param contentLength how long the body is supposed to be */ - public EventBuilder withBody(InputStream body, int contentLength) { + public EventBuilder withBody(InputStream body) { Objects.requireNonNull(body, "body"); - if (contentLength < 0) { - throw new IllegalArgumentException("Invalid contentLength"); - } - // This is for safety. Because we concatenate events, an input stream shorter than content length will cause - // the implementation to continue reading through to the next http request. We need to avoid a sort of - // buffer overrun. - // FIXME: Make InputStream handling simpler. - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sib.bind(body); - this.body = new ContentLengthInputStream(sib, contentLength); - this.contentLength = contentLength; + + this.body = body; return this; } @@ -119,7 +132,7 @@ public EventBuilder withBody(InputStream body, int contentLength) { */ public EventBuilder withBody(byte[] body) { Objects.requireNonNull(body, "body"); - return withBody(new ByteArrayInputStream(body), body.length); + return withBody(new ByteArrayInputStream(body)); } /** @@ -132,116 +145,79 @@ public EventBuilder withBody(String body) { return withBody(stringAsBytes); } - /** - * Set the body of the request from a stream - * @param contentStream the content of the body - */ - public EventBuilder withBody(InputStream contentStream) throws IOException { - return withBody(IOUtils.toByteArray(contentStream)); - } /** - * Set the fn route associated with the call + * Prepare an event for the configured codec - this sets appropriate environment variable in the Env mock and StdIn mocks. + *

* - * @param route the route + * @throws IllegalStateException If the the codec only supports one event and an event has already been enqueued. */ - public EventBuilder withRoute(String route) { - Objects.requireNonNull(route, "route"); - this.route = route; - return this; - } + public void enqueue() { + InputEvent event = new ReadOnceInputEvent(body, headers, callID, deadline); + pendingInput.add(event); - /** - * Set the HTTP method of the incoming request - * - * @param method an HTTP method - * @return - */ - public EventBuilder withMethod(String method) { - Objects.requireNonNull(method, "method"); - this.method = method.toUpperCase(); - return this; } - /** - * Set the app name the incoming event - * - * @param appName the app name - * @return - */ - public EventBuilder withAppName(String appName) { - Objects.requireNonNull(appName, "appName"); - this.appName = appName; - return this; + Map commonEnv() { + Map env = new HashMap<>(config); + env.put("FN_APP_ID", "appID"); + env.put("FN_FN_ID", "fnID"); + + return env; } + } - /** - * Set the request URL of the incoming event - * - * @param requestUrl the request URL - * @return - */ - public EventBuilder withRequestUrl(String requestUrl) { - Objects.requireNonNull(requestUrl, "requestUrl"); - this.requestUrl = requestUrl; - return this; + static class TestOutput implements OutputEvent { + private final OutputEvent from; + byte[] body; + + TestOutput(OutputEvent from) throws IOException { + this.from = from; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + from.writeToOutput(bos); + body = bos.toByteArray(); } - /** - * Prepare an event for the configured codec - this sets appropriate environment variable in the Env mock and StdIn mocks. - *

- * - * @throws IllegalStateException If the the codec only supports one event and an event has already been enqueued. - */ - public abstract void enqueue(); + @Override + public Status getStatus() { + return from.getStatus(); + } - Map commonEnv() { - Map env = new HashMap<>(); - env.putAll(config); - headers.forEach((k, v) -> { - env.put("FN_HEADER_" + k.toUpperCase().replaceAll("-", "_"), v); - }); - env.put("FN_METHOD", method); - env.put("FN_APP_NAME", appName); - env.put("FN_PATH", route); - env.put("FN_REQUEST_URL", requestUrl); - return env; + @Override + public Optional getContentType() { + return from.getContentType(); } - } - private final class HttpEventBuilder extends EventBuilder { @Override - public void enqueue() { - StringBuilder inputString = new StringBuilder(); - // Only set env for first event. - if (!hasEvents) { - commonEnv().forEach(vars::put); - vars.put("FN_FORMAT", "http"); - } - inputString.append(method); - inputString.append(" / HTTP/1.1\r\n"); - inputString.append("Fn_App_name: ").append(appName).append("\r\n"); - inputString.append("Fn_Method: ").append(method).append("\r\n"); - inputString.append("Fn_Path: ").append(route).append("\r\n"); - inputString.append("Fn_Request_url: ").append(requestUrl).append("\r\n"); - if (contentType != null) { - inputString.append("Content-Type: ").append(contentType).append("\r\n"); - } + public Headers getHeaders() { + return from.getHeaders(); + } - inputString.append("Content-length: ").append(Integer.toString(contentLength)).append("\r\n"); - headers.forEach((k, v) -> { - inputString.append(k).append(": ").append(v).append("\r\n"); - }); + @Override + public void writeToOutput(OutputStream out) throws IOException { + out.write(body); + } - // added to the http request as headers to mimic the behaviour of `functions` but should NOT be used as config - config.forEach((k, v) -> { - inputString.append(k).append(": ").append(v).append("\r\n"); - }); - inputString.append("\r\n"); + public byte[] getBody() { + return body; + } - pendingInput = new SequenceInputStream(pendingInput, new ByteArrayInputStream(inputString.toString().getBytes())); - pendingInput = new SequenceInputStream(pendingInput, body); - hasEvents = true; + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TestOutput{"); + sb.append("body="); + if (body == null) sb.append("null"); + else { + sb.append('['); + for (int i = 0; i < body.length; ++i) + sb.append(i == 0 ? "" : ", ").append(body[i]); + sb.append(']'); + } + sb.append(", status=").append(getStatus()); + sb.append(", contentType=").append(getContentType()); + sb.append(", headers=").append(getHeaders()); + sb.append('}'); + return sb.toString(); } } @@ -255,6 +231,27 @@ public void thenRun(Class cls, String method) { thenRun(cls.getName(), method); } + static class TestCodec implements EventCodec { + private final List input; + private final List output; + + TestCodec(List input, List output) { + this.input = input; + this.output = output; + } + + @Override + public void runCodec(Handler h) { + for (InputEvent in : input) { + try { + output.add(new TestOutput(h.handle(in))); + } catch (IOException e) { + throw new RuntimeException("Unexpected exception in test", e); + } + } + } + } + /** * Runs the function runtime with the specified class and method * @@ -266,17 +263,21 @@ public void thenRun(String cls, String method) { PrintStream oldSystemOut = System.out; PrintStream oldSystemErr = System.err; try { - PrintStream functionOut = new PrintStream(stdOut); PrintStream functionErr = new PrintStream(new TeeOutputStream(stdErr, oldSystemErr)); System.setOut(functionErr); System.setErr(functionErr); + + Map fnConfig = new HashMap<>(config); + fnConfig.put("FN_APP_ID", "appID"); + fnConfig.put("FN_FORMAT", "http-stream"); + fnConfig.put("FN_FN_ID", "fnID"); + + exitStatus = new EntryPoint().run( - vars, - pendingInput, - functionOut, - functionErr, - cls + "::" + method); - stdOut.flush(); + fnConfig, + new TestCodec(pendingInput, output), + functionErr, + cls + "::" + method); stdErr.flush(); } catch (Exception e) { throw new RuntimeException(e); @@ -305,7 +306,7 @@ public int exitStatus() { * @return a new event builder. */ public EventBuilder givenEvent() { - return new HttpEventBuilder(); + return new EventBuilder(); } @@ -334,117 +335,10 @@ public String getStdErrAsString() { * * @return the bytes returned by the function runtime; */ - public byte[] getStdOut() { - return stdOut.toByteArray(); - } - - /** - * Get the output produced by the runtime as a string. - *

- * For Hot functions this will include the HTTP envelope with (possibly) multiple messages - * - * @return a string representation of the function output - */ - public String getStdOutAsString() { - return new String(stdOut.toByteArray()); - } - - - /** - * A simple abstraction for a parsed HTTP response returned by a hot function - */ - public interface ParsedHttpResponse { - /** - * Return the body of the function result as a byte array - * - * @return the function response body - */ - byte[] getBodyAsBytes(); - - /** - * return the body of the function response as a string - * - * @return a function response body - */ - String getBodyAsString(); - - /** - * A map of he headers returned by the function - *

- * These are squashed so duplicated headers will be ignored (takes the first header) - * - * @return a map of headers - */ - Map getHeaders(); - - /** - * @return the HTTP status code returned by the function - */ - int getStatus(); - } - - /** - * Parses any pending HTTP responses on the functions stdout stream - * - * @return a list of Parsed HTTP responses from the function runtime output; - */ - public List getParsedHttpResponses() { - return getParsedHttpResponses(stdOut.toByteArray()); + public List getOutputs() { + return output; } - public static List getParsedHttpResponses(byte[] streamAsBytes) { - - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - ByteArrayInputStream parseStream = new ByteArrayInputStream(streamAsBytes); - sib.bind(parseStream); - - DefaultHttpResponseParser parser = new DefaultHttpResponseParser(sib); - List responses = new ArrayList<>(); - - while (true) { - try { - HttpResponse response = parser.parse(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ContentLengthInputStream cis = new ContentLengthInputStream(sib, Long.parseLong(response.getFirstHeader("Content-length").getValue())); - - IOUtils.copy(cis, bos); - cis.close(); - byte[] body = bos.toByteArray(); - ParsedHttpResponse r = new ParsedHttpResponse() { - @Override - public byte[] getBodyAsBytes() { - return body; - } - - @Override - public String getBodyAsString() { - return new String(body); - } - - @Override - public Map getHeaders() { - Map headers = new HashMap<>(); - Arrays.stream(response.getAllHeaders()).forEach((h) -> { - headers.put(h.getName(), h.getValue()); - }); - return headers; - } - - @Override - public int getStatus() { - return response.getStatusLine().getStatusCode(); - } - }; - responses.add(r); - } catch (NoHttpResponseException e) { - break; - } catch (Exception e) { - throw new RuntimeException("Invalid HTTP response", e); - } - } - return responses; - - } @Override public Statement apply(Statement base, Description description) { @@ -452,34 +346,5 @@ public Statement apply(Statement base, Description description) { } - private final class DefaultEventBuilder extends EventBuilder { - boolean sent = false; - - @Override - public void enqueue() { - if (sent) { - throw new IllegalStateException("Cannot enqueue multiple default events "); - } - pendingInput = body; - sent = true; - commonEnv().forEach(vars::put); - } - } - - /** - * mock a default event (Input and stdOut encoded as stdin/stdout) - * - * @return a new event builder. - */ - public EventBuilder givenDefaultEvent() { - return new DefaultEventBuilder(); - } - - /** - * mock an http event - */ - public EventBuilder givenHttpEvent() { - return new HttpEventBuilder(); - } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTests.java b/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTest.java similarity index 56% rename from runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTests.java rename to runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTest.java index 4d78c9e1..b0b26ba8 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTests.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/FunctionConstructionTest.java @@ -1,6 +1,24 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; +import com.fnproject.fn.api.OutputEvent; import com.fnproject.fn.runtime.testfns.TestFnConstructors; +import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; @@ -9,81 +27,87 @@ /** * End-to-end tests for function configuration methods */ -public class FunctionConstructionTests { +public class FunctionConstructionTest { @Rule public final FnTestHarness fn = new FnTestHarness(); @Test public void shouldConstructWithDefaultConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.DefaultEmptyConstructor.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(0); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @Test public void shouldConstructWithExplicitConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.ExplicitEmptyConstructor.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(0); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @Test public void shouldInjectConfigIntoConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.ConfigurationOnConstructor.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(0); - assertThat(fn.getStdOutAsString()).isEqualTo("OK"); + assertThat(fn.getOnlyOutputAsString()).isEqualTo("OK"); } @Test public void shouldFailWithInaccessibleConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorNotAccessible.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as it has no public constructors"); } @Test public void shouldFailFunctionWithTooManyConstructorArgs() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorTooManyArgs.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as its constructor takes more than one argument"); } @Test public void shouldFailFunctionWithAmbiguousConstructors() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorAmbiguousConstructors.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as it has multiple public constructors"); } @Test public void shouldFailFunctionWithErrorInConstructor() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorThrowsException.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains("An error occurred in the function constructor while instantiating class"); } @Test public void shouldFailFunctionWithBadSingleConstructConstructorArg() { - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.BadConstructorUnrecognisedArg.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as its constructor takes an unrecognized argument of type int"); } @Test public void shouldFailNonStaticInnerClassWithANiceMessage(){ - fn.givenDefaultEvent().enqueue(); + fn.givenEvent().enqueue(); fn.thenRun(TestFnConstructors.NonStaticInnerClass.class, "invoke"); assertThat(fn.exitStatus()).isEqualTo(2); + assertThat(fn.getOutputs()).hasSize(1).allSatisfy(testOutput -> Assertions.assertThat(testOutput.getStatus()).isEqualTo(OutputEvent.Status.FunctionError)); assertThat(fn.getStdErrAsString()).contains("cannot be instantiated as it is a non-static inner class"); } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/HTTPStreamCodecTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/HTTPStreamCodecTest.java new file mode 100644 index 00000000..7e68816d --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/HTTPStreamCodecTest.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime; + + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.attribute.PosixFilePermissions; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; +import org.apache.commons.io.IOUtils; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BytesContentProvider; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.unixsocket.client.HttpClientTransportOverUnixSockets; +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.Timeout; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This uses the Jetty client largely as witness of "good HTTP behaviour" + * Created on 24/08/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class HTTPStreamCodecTest { + + + private static final String VERSION = "FDK_TEST_VERSION"; + private static final String RUNTIME_VERSION = "FDK_TEST_RUNTIME"; + @Rule + public final Timeout to = Timeout.builder().withTimeout(60, TimeUnit.SECONDS).withLookingForStuckThread(true).build(); + + private static final Map defaultEnv; + private final List cleanups = new ArrayList<>(); + + private static File generateSocketFile() { + File f; + try { + f = File.createTempFile("socket", ".sock"); + f.delete(); + f.deleteOnExit(); + } catch (IOException e) { + throw new RuntimeException("Error creating socket file", e); + } + + return f; + } + + static { + if (System.getenv("RUNTIME_BUILD_DIR") == null) { + System.setProperty("com.fnproject.java.native.libdir", new File("src/main/c/").getAbsolutePath()); + }else{ + System.setProperty("com.fnproject.java.native.libdir", new File(System.getenv("RUNTIME_BUILD_DIR")).getAbsolutePath()); + } + + System.setProperty("org.eclipse.jetty.util.log.class", "org.eclipse.jetty.util.log.StdErrLog"); + System.setProperty("org.eclipse.jetty.LEVEL", "WARN"); + + Map env = new HashMap<>(); + env.put("FN_APP_NAME", "myapp"); + env.put("FN_PATH", "mypath"); + + defaultEnv = Collections.unmodifiableMap(env); + } + + private HttpClient createClient(File unixSocket) throws Exception { + HttpClient client = new HttpClient(new HttpClientTransportOverUnixSockets(unixSocket.getAbsolutePath()), null); + client.start(); + cleanups.add(() -> { + try { + client.stop(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + return client; + } + + private Request defaultRequest(HttpClient httpClient) { + return httpClient.newRequest("http://localhost/call") + .method("POST") + .header("Fn-Call-Id", "callID") + .header("Fn-Deadline", "2002-10-02T10:00:00.992Z") + .header("Custom-header", "v1") + .header("Custom-header", "v2") + .header("Content-Type", "text/plain") + .content(new StringContentProvider("hello ")); + + } + + + @After + public void cleanup() { + cleanups.forEach(Runnable::run); + } + + File startCodec(Map env, EventCodec.Handler h) { + Map newEnv = new HashMap<>(env); + File socket = generateSocketFile(); + newEnv.put("FN_LISTENER", "unix:" + socket.getAbsolutePath()); + + HTTPStreamCodec codec = new HTTPStreamCodec(newEnv, VERSION, RUNTIME_VERSION); + + Thread t = new Thread(() -> codec.runCodec(h)); + t.start(); + cleanups.add(() -> { + try { + codec.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + return socket; + } + + @Test + public void shouldAcceptDataOnHttp() throws Exception { + CompletableFuture lastEvent = new CompletableFuture<>(); + + File socketFile = startCodec(defaultEnv, (in) -> { + lastEvent.complete(in); + return OutputEvent.fromBytes("hello".getBytes(), OutputEvent.Status.Success, "text/plain", Headers.emptyHeaders().addHeader("x-test", "bar")); + }); + + HttpClient client = createClient(socketFile); + ContentResponse resp = client.newRequest("http://localhost/call") + .method("POST") + .header("Fn-Call-Id", "callID") + .header("Fn-Deadline", "2002-10-02T10:00:00.992Z") + .header("Custom-header", "v1") + .header("Custom-header", "v2") + .header("Content-Type", "text/plain") + .content(new StringContentProvider("hello ")).send(); + + assertThat(resp.getStatus()).isEqualTo(200); + assertThat(resp.getContent()).isEqualTo("hello".getBytes()); + assertThat(resp.getHeaders().get("x-test")).isEqualTo("bar"); + assertThat(resp.getHeaders().get("fn-fdk-version")).isEqualTo(VERSION); + assertThat(resp.getHeaders().get("fn-fdk-runtime")).isEqualTo(RUNTIME_VERSION); + + InputEvent evt = lastEvent.get(1, TimeUnit.MILLISECONDS); + assertThat(evt.getCallID()).isEqualTo("callID"); + assertThat(evt.getDeadline().toEpochMilli()).isEqualTo(1033552800992L); + assertThat(evt.getHeaders()).isEqualTo(Headers.emptyHeaders().addHeader("Fn-Call-Id", "callID").addHeader("Fn-Deadline", "2002-10-02T10:00:00.992Z").addHeader("Custom-header", "v1", "v2").addHeader("Content-Type", "text/plain").addHeader("Content-Length", "6")); + + } + + + @Test + public void shouldRejectFnMissingHeaders() throws Exception { + + Map headers = new HashMap<>(); + headers.put("Fn-Call-Id", "callID"); + headers.put("Fn-Deadline", "2002-10-02T10:00:00.992Z"); + + + File socket = startCodec(defaultEnv, (in) -> OutputEvent.emptyResult(OutputEvent.Status.Success)); + + HttpClient client = createClient(socket); + + Request positive = client.newRequest("http://localhost/call") + .method("POST"); + headers.forEach(positive::header); + assertThat(positive.send().getStatus()).withFailMessage("Expecting req with mandatory headers to pass").isEqualTo(200); + + for (String h : headers.keySet()) { + Request r = client.newRequest("http://localhost/call") + .method("POST"); + headers.forEach((k, v) -> { + if (!k.equals(h)) { + r.header(k, v); + } + }); + + + ContentResponse resp = r.send(); + + assertThat(resp.getStatus()).withFailMessage("Expected failure error code for missing header " + h).isEqualTo(500); + + } + } + + @Test + public void shouldHandleMultipleRequests() throws Exception { + AtomicReference lastInput = new AtomicReference<>(); + AtomicInteger count = new AtomicInteger(0); + + File socket = startCodec(defaultEnv, (in) -> { + byte[] body = in.consumeBody((is) -> { + try { + return IOUtils.toByteArray(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + lastInput.set(body); + return OutputEvent.fromBytes(body, OutputEvent.Status.Success, "application/octet-stream", Headers.emptyHeaders()); + }); + + HttpClient httpClient = createClient(socket); + + for (int i = 0; i < 200; i++) { + byte[] body = randomBytes(i * 1997); + ContentResponse resp = httpClient.newRequest("http://localhost/call") + .method("POST") + .header("Fn-Call-Id", "callID") + .header("Fn-Deadline", "2002-10-02T10:00:00.992Z") + .header("Custom-header", "v1") + .header("Custom-header", "v2") + .header("Content-Type", "text/plain") + .content(new BytesContentProvider(body)).send(); + + assertThat(resp.getStatus()).isEqualTo(200); + assertThat(resp.getContent()).isEqualTo(body); + assertThat(lastInput).isNotNull(); + assertThat(lastInput.get()).isEqualTo(body); + } + + } + + @Test + public void shouldHandleLargeBodies() throws Exception { + // Round trips 10 meg of data through the codec and validates it got the right stuff back + byte[] randomString = randomBytes(1024 * 1024 * 10); + byte[] inDigest = MessageDigest.getInstance("SHA-256").digest(randomString); + + + File socket = startCodec(defaultEnv, (in) -> { + byte[] content = in.consumeBody((is) -> { + try { + return IOUtils.toByteArray(is); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + return OutputEvent.fromBytes(content, OutputEvent.Status.Success, "application/octet-binary", Headers.emptyHeaders()); + }); + + HttpClient client = createClient(socket); + + CompletableFuture cdl = new CompletableFuture<>(); + MessageDigest readDigest = MessageDigest.getInstance("SHA-256"); + defaultRequest(client) + .content(new BytesContentProvider(randomString)) + .onResponseContent((response, byteBuffer) -> readDigest.update(byteBuffer)) + .send(cdl::complete); + Result r = cdl.get(); + assertThat(r.getResponse().getStatus()).isEqualTo(200); + assertThat(readDigest.digest()).isEqualTo(inDigest); + } + + private byte[] randomBytes(int sz) { + Random sr = new Random(); + byte[] part = new byte[997]; + sr.nextBytes(part); + + // Make random ascii for convenience in debugging + for (int i = 0; i < part.length; i++) { + part[i] = (byte) ((part[i] % 26) + 65); + } + + byte[] randomString = new byte[sz]; + int left = sz; + for (int i = 0; i < randomString.length; i += part.length) { + int copy = Math.min(left, part.length); + + System.arraycopy(part, 0, randomString, i, copy); + left -= part.length; + } + return randomString; + } + + @Test + public void shouldConvertStatusResponses() throws Exception { + + for (OutputEvent.Status s : OutputEvent.Status.values()) { + CompletableFuture lastEvent = new CompletableFuture<>(); + + File socket = startCodec(defaultEnv, (in) -> { + lastEvent.complete(in); + return OutputEvent.fromBytes("hello".getBytes(), s, "text/plain", Headers.emptyHeaders()); + }); + + HttpClient client = createClient(socket); + + ContentResponse resp = defaultRequest(client).send(); + + assertThat(resp.getStatus()).isEqualTo(s.getCode()); + } + } + + @Test + public void shouldStripHopToHopHeadersFromFunctionInput() throws Exception { + + for (String[] header : new String[][] { + {"Transfer-encoding", "chunked"}, + {"Connection", "close"}, + }) { + CompletableFuture lastEvent = new CompletableFuture<>(); + + File socket = startCodec(defaultEnv, (in) -> { + lastEvent.complete(in); + return OutputEvent.fromBytes("hello".getBytes(), OutputEvent.Status.Success, "text/plain", Headers.emptyHeaders().addHeader(header[0], header[1])); + }); + HttpClient client = createClient(socket); + ContentResponse resp = defaultRequest(client).send(); + + assertThat(resp.getHeaders().get(header[0])).isNull(); + } + } + + @Test + public void socketShouldHaveCorrectPermissions() throws Exception { + File listener = startCodec(defaultEnv, (in) -> OutputEvent.fromBytes("hello".getBytes(), OutputEvent.Status.Success, "text/plain", Headers.emptyHeaders())); + assertThat(Files.getPosixFilePermissions(listener.toPath())).isEqualTo(PosixFilePermissions.fromString("rw-rw-rw-")); + + cleanup(); + } + + +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java b/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java index bc5c4895..9b684725 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/HeaderBuilder.java @@ -1,10 +1,30 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; +import com.fnproject.fn.api.Headers; + import java.util.AbstractMap; +import java.util.Arrays; +import java.util.List; import java.util.Map; class HeaderBuilder { - static Map.Entry headerEntry(String key, String value) { - return new AbstractMap.SimpleEntry<>(key, value); + static Map.Entry> headerEntry(String key, String... values) { + return new AbstractMap.SimpleEntry<>(Headers.canonicalKey(key), Arrays.asList(values)); } } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/HttpEventCodecTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/HttpEventCodecTest.java deleted file mode 100644 index 7bed3d44..00000000 --- a/runtime/src/test/java/com/fnproject/fn/runtime/HttpEventCodecTest.java +++ /dev/null @@ -1,264 +0,0 @@ -package com.fnproject.fn.runtime; - -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.exception.FunctionInputHandlingException; -import org.apache.commons.io.input.NullInputStream; -import org.apache.commons.io.output.NullOutputStream; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Map; -import java.util.stream.Collectors; - -import static com.fnproject.fn.runtime.HeaderBuilder.headerEntry; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.entry; -import static org.junit.Assert.fail; - -public class HttpEventCodecTest { - private final OutputStream nullOut = new NullOutputStream(); - private final InputStream nullIn = new NullInputStream(0); - - private final String postReq = "GET /test HTTP/1.1\n" + - "Accept-Encoding: gzip\n" + - "User-Agent: useragent\n" + - "Accept: text/html, text/plain;q=0.9\n" + - "Fn_Request_url: http//localhost:8080/r/testapp/test\n" + - "Fn_Path: /test\n" + - "Fn_Method: POST\n" + - "Content-Length: 11\n" + - "Fn_App_name: testapp\n" + - "Fn_Call_id: task-id\n" + - "Myconfig: fooconfig\n" + - "Content-Type: text/plain\n\n" + - "Hello World"; - - - private final String getReq = "GET /test HTTP/1.1\n" + - "Accept-Encoding: gzip\n" + - "User-Agent: useragent\n" + - "Fn_Request_url: http//localhost:8080/r/testapp/test\n" + - "Fn_Path: /test2\n" + - "Fn_Method: GET\n" + - "Content-Length: 0\n" + - "Fn_App_name: testapp\n" + - "Fn_Call_Id: task-id2\n" + - "Myconfig: fooconfig\n\n"; - - private final Map emptyConfig = new HashMap<>(); - - @Test - public void testParsingSimpleHttpRequestWithFnHeadersAndBody() { - ByteArrayInputStream bis = new ByteArrayInputStream(postReq.getBytes()); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - HttpEventCodec httpEventCodec = new HttpEventCodec(bis, bos); - - InputEvent event = httpEventCodec.readEvent().get(); - isExpectedPostEvent(event); - } - - @Test - public void shouldReadMultipleRequestsOnSameStream() { - byte req1[] = postReq.getBytes(); - byte req2[] = getReq.getBytes(); - byte req3[] = postReq.getBytes(); - - byte input[] = new byte[req1.length + req2.length + req3.length]; - - System.arraycopy(req1, 0, input, 0, req1.length); - System.arraycopy(req2, 0, input, req1.length, req2.length); - System.arraycopy(req3, 0, input, req1.length + req2.length, req3.length); - - ByteArrayInputStream bis = new ByteArrayInputStream(input); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - HttpEventCodec httpEventCodec = new HttpEventCodec(bis, bos); - - InputEvent postEvent = httpEventCodec.readEvent().get(); - isExpectedPostEvent(postEvent); - - InputEvent getEvent = httpEventCodec.readEvent().get(); - isExpectedGetEvent(getEvent); - - InputEvent postEvent2 = httpEventCodec.readEvent().get(); - isExpectedPostEvent(postEvent2); - - - } - - @Test - public void shouldRejectInvalidHttpRequest() { - try { - HttpEventCodec httpEventCodec = new HttpEventCodec(asStream("NOT_HTTP " + getReq), nullOut); - - httpEventCodec.readEvent(); - fail(); - } catch (FunctionInputHandlingException e) { - assertThat(e).hasMessageContaining("Failed to read HTTP content from input"); - } - } - - - @Test - public void shouldRejectMissingHttpHeaders() { - Map requiredHeaders = new HashMap<>(); - - requiredHeaders.put("fn_request_url", "request_url"); - requiredHeaders.put("fn_path", "/route"); - requiredHeaders.put("fn_method", "GET"); - requiredHeaders.put("fn_app_name", "app_name"); - - for (String key : requiredHeaders.keySet()) { - Map newMap = new HashMap<>(requiredHeaders); - newMap.remove(key); - String req = "GET / HTTP/1.1\n" + newMap.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue()).collect(Collectors.joining("\n")) + "\n\n"; - - try { - HttpEventCodec httpEventCodec = new HttpEventCodec(asStream(req), nullOut); - httpEventCodec.readEvent(); - fail("Should fail with header missing:" + key); - } catch (FunctionInputHandlingException e) { - assertThat(e).hasMessageMatching("Incoming HTTP frame is missing required header: " + key); - } - } - - } - - @Test - public void shouldSerializeSimpleSuccessfulEvent() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(nullIn,bos); - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(),OutputEvent.SUCCESS,"text/plain"); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 200 INVOKED"); - assertThat(headers(httpResponse)).containsOnly(entry("content-type", "text/plain"), entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - - } - - private static String statusLine(String httpResponse) { - return httpResponse.split("\\\r\\\n", 2)[0]; - } - - private static Map headers(String httpResponse) { - Map hs = new HashMap<>(); - boolean firstLine = true; - for (String line: httpResponse.split("\\\r\\\n")) { - if (line.equals("")) { break; } - if (firstLine) { - firstLine = false; - continue; - } - String[] parts = line.split(": *", 2); - hs.put(parts[0].toLowerCase(), parts[1]); - } - return hs; - } - - private static String body(String httpResponse) { - return httpResponse.split("\\\r\\\n\\\r\\\n", 2)[1]; - } - - @Test - public void shouldSerializeSuccessfulEventWithHeaders() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(nullIn,bos); - Map hs = new HashMap<>(); - hs.put("foo", "bar"); - hs.put("Content-Type", "application/octet-stream"); // ignored - hs.put("Content-length", "99"); // ignored - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(),OutputEvent.SUCCESS,"text/plain", Headers.fromMap(hs)); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 200 INVOKED"); - assertThat(headers(httpResponse)).containsOnly(entry("foo", "bar"), - entry("content-type", "text/plain"), - entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - } - - - @Test - public void shouldSerializeSimpleFailedEvent() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(nullIn,bos); - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(), OutputEvent.FAILURE,"text/plain"); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 500 INVOKE FAILED"); - assertThat(headers(httpResponse)).containsOnly(entry("content-type", "text/plain"), - entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - } - - - @Test - public void shouldSerializeFailedEventWithHeaders() throws Exception{ - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - - HttpEventCodec httpEventCodec = new HttpEventCodec(nullIn,bos); - Map hs = new HashMap<>(); - hs.put("foo", "bar"); - hs.put("Content-Type", "application/octet-stream"); // ignored - hs.put("Content-length", "99"); // ignored - OutputEvent outEvent = OutputEvent.fromBytes("Hello".getBytes(), OutputEvent.FAILURE,"text/plain", Headers.fromMap(hs)); - - httpEventCodec.writeEvent(outEvent); - String httpResponse = new String(bos.toByteArray()); - - assertThat(statusLine(httpResponse)).isEqualTo("HTTP/1.1 500 INVOKE FAILED"); - assertThat(headers(httpResponse)).containsOnly(entry("foo", "bar"), - entry("content-type", "text/plain"), - entry("content-length", "5")); - assertThat(body(httpResponse)).isEqualTo("Hello"); - } - - - private InputStream asStream(String sin) { - return new ByteArrayInputStream(sin.getBytes()); - } - - private void isExpectedGetEvent(InputEvent getEvent) { - assertThat(getEvent.getAppName()).isEqualTo("testapp"); - assertThat(getEvent.getMethod()).isEqualTo("GET"); - assertThat(getEvent.getRoute()).isEqualTo("/test2"); - - assertThat(getEvent.getHeaders().getAll()) - .contains(headerEntry("Accept-Encoding", "gzip"), - headerEntry("User-Agent", "useragent")); - - - getEvent.consumeBody((is) -> assertThat(is).hasSameContentAs(asStream(""))); - } - - private void isExpectedPostEvent(InputEvent postEvent) { - assertThat(postEvent.getAppName()).isEqualTo("testapp"); - assertThat(postEvent.getMethod()).isEqualTo("POST"); - assertThat(postEvent.getRoute()).isEqualTo("/test"); - assertThat(postEvent.getHeaders().getAll().size()).isEqualTo(11); - assertThat(postEvent.getHeaders().getAll()) - .contains(headerEntry("Accept", "text/html, text/plain;q=0.9"), - headerEntry("Accept-Encoding", "gzip"), - headerEntry("User-Agent", "useragent"), - headerEntry("Content-Type", "text/plain")); - - postEvent.consumeBody((is) -> assertThat(is).hasSameContentAs(asStream("Hello World"))); - } - - -} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java index 28124dbb..49edd95e 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/JacksonCoercionTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.api.Headers; @@ -9,7 +25,11 @@ import org.junit.Test; import java.io.ByteArrayInputStream; -import java.util.*; +import java.time.Instant; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; public class JacksonCoercionTest { @@ -24,13 +44,13 @@ public void listOfCustomObjects() throws NoSuchMethodException { MethodWrapper method = new DefaultMethodWrapper(JacksonCoercionTest.class, "testMethod"); FunctionRuntimeContext frc = new FunctionRuntimeContext(method, new HashMap<>()); - FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc); + FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc,new ReadOnceInputEvent(new ByteArrayInputStream(new byte[0]),Headers.emptyHeaders(),"callID",Instant.now())); Map headers = new HashMap<>(); headers.put("content-type", "application/json"); ByteArrayInputStream body = new ByteArrayInputStream("[{\"name\":\"Spot\",\"age\":6},{\"name\":\"Jason\",\"age\":16}]".getBytes()); - InputEvent inputEvent = new ReadOnceInputEvent("", "", "", "testMethod", body, Headers.fromMap(headers), new QueryParametersImpl()); + InputEvent inputEvent = new ReadOnceInputEvent( body, Headers.fromMap(headers),"call",Instant.now()); Optional object = jc.tryCoerceParam(invocationContext, 0, inputEvent, method); @@ -46,13 +66,13 @@ public void failureToParseIsUserFriendlyError() throws NoSuchMethodException { MethodWrapper method = new DefaultMethodWrapper(JacksonCoercionTest.class, "testMethod"); FunctionRuntimeContext frc = new FunctionRuntimeContext(method, new HashMap<>()); - FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc); + FunctionInvocationContext invocationContext = new FunctionInvocationContext(frc,new ReadOnceInputEvent(new ByteArrayInputStream(new byte[0]),Headers.emptyHeaders(),"callID",Instant.now())); Map headers = new HashMap<>(); headers.put("content-type", "application/json"); ByteArrayInputStream body = new ByteArrayInputStream("INVALID JSON".getBytes()); - InputEvent inputEvent = new ReadOnceInputEvent("", "", "", "testMethod", body, Headers.fromMap(headers), new QueryParametersImpl()); + InputEvent inputEvent = new ReadOnceInputEvent( body, Headers.fromMap(headers), "call",Instant.now()); boolean causedExpectedError; try { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java b/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java index 2d4cca57..c1f1047d 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/MethodWrapperTests.java @@ -1,6 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.api.MethodWrapper; +import org.assertj.core.api.AbstractIntegerAssert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -8,7 +25,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.List; -import java.util.function.Function; import static org.assertj.core.api.Assertions.assertThat; @@ -35,8 +51,9 @@ public void testMethodParameterHasExpectedType() throws NoSuchMethodException { if (parameterIndex >= 0) { assertThat(method.getParamType(parameterIndex).getParameterClass()).isEqualTo(expectedType); } else { - assertThat(parameterIndex).isEqualTo(-1) - .withFailMessage("You can only use non negative parameter indices or -1 to represent return value in this test suite"); + AbstractIntegerAssert withFailMessage = assertThat(parameterIndex) + .isEqualTo(-1) + .withFailMessage("You can only use non negative parameter indices or -1 to represent return value in this test suite"); assertThat(method.getReturnType().getParameterClass()).isEqualTo(expectedType); } } @@ -68,20 +85,20 @@ public static Collection data() throws Exception { } static class ConcreteTypeExamples { - public void voidReturnType() { }; - public void singleParameter(String s) { }; - public void singlePrimitiveParameter(boolean i) { }; - public void singlePrimitiveParameter(byte i) { }; - public void singlePrimitiveParameter(char i) { }; - public void singlePrimitiveParameter(short i) { }; - public void singlePrimitiveParameter(int i) { }; - public void singlePrimitiveParameter(long i) { }; - public void singlePrimitiveParameter(float i) { }; - public void singlePrimitiveParameter(double i) { }; - public void multipleParameters(String s, double i) { }; - public String noArgs() { return ""; }; - public int noArgsWithPrimitiveReturnType() { return 1; }; - public void singleGenericParameter(List s) { }; + public void voidReturnType() { } + public void singleParameter(String s) { } + public void singlePrimitiveParameter(boolean i) { } + public void singlePrimitiveParameter(byte i) { } + public void singlePrimitiveParameter(char i) { } + public void singlePrimitiveParameter(short i) { } + public void singlePrimitiveParameter(int i) { } + public void singlePrimitiveParameter(long i) { } + public void singlePrimitiveParameter(float i) { } + public void singlePrimitiveParameter(double i) { } + public void multipleParameters(String s, double i) { } + public String noArgs() { return ""; } + public int noArgsWithPrimitiveReturnType() { return 1; } + public void singleGenericParameter(List s) { } } static class ParentClassWithGenericType { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java index 2714b449..14cb92b7 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/QueryParametersParserTest.java @@ -1,9 +1,29 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime; import com.fnproject.fn.api.QueryParameters; +import com.fnproject.fn.runtime.httpgateway.QueryParametersParser; import org.junit.Test; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; @@ -17,7 +37,7 @@ public void noUrlParametersProducesEmptyMap() { } @Test - public void gettingNonExistantParameterProducesOptionalEmpty() { + public void gettingNonExistentParameterProducesOptionalEmpty() { QueryParameters params = QueryParametersParser.getParams("www.example.com"); assertThat(params.getValues("var")).isEmpty(); diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsTest.java deleted file mode 100644 index 03e07a1c..00000000 --- a/runtime/src/test/java/com/fnproject/fn/runtime/flow/FlowsTest.java +++ /dev/null @@ -1,280 +0,0 @@ -package com.fnproject.fn.runtime.flow; - -import com.fnproject.fn.runtime.FnTestHarness; -import com.fnproject.fn.runtime.testfns.FnFlowsFunction; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import java.util.concurrent.atomic.AtomicBoolean; - -import static org.mockito.Mockito.*; - -public class FlowsTest { - - @Rule - public FnTestHarness fnTestHarness = new FnTestHarness(); - - private final String FUNCTION_ID = "app/testfn"; - private final FlowId FLOW_ID = new FlowId("test-flow-id"); - - // static to avoid issues with serialized AtomicRefs - static AtomicBoolean tag = new AtomicBoolean(false); - - @Mock - CompleterClient mockCompleterClient; - - TestBlobStore testBlobStore; - - @Before - public void setup() { - tag.set(false); - MockitoAnnotations.initMocks(this); - FlowRuntimeGlobals.resetCompleterClientFactory(); - - FlowRuntimeGlobals.setCompleterClientFactory(new CompleterClientFactory() { - @Override - public CompleterClient getCompleterClient() { - return mockCompleterClient; - } - - @Override - public BlobStoreClient getBlobStoreClient() { - return testBlobStore; - } - }); - } - - private FnTestHarness.EventBuilder eventToTestFN() { - return fnTestHarness.givenDefaultEvent().withAppName("app").withRoute("/testfn"); - } - - private FnTestHarness.EventBuilder httpEventToTestFN() { - return fnTestHarness.givenHttpEvent() - .withAppName("app") - .withRoute("/testfn"); - } - - @Test - public void completerNotCalledIfFlowRuntimeUnused() throws Exception { - - eventToTestFN().enqueue(); - fnTestHarness.thenRun(FnFlowsFunction.class, "notUsingFlows"); - - verify(mockCompleterClient, never()).createFlow(any()); - } - - @Test - public void completerCalledWhenFlowRuntimeIsAccessed() { - - when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); - - eventToTestFN().enqueue(); - fnTestHarness.thenRun(FnFlowsFunction.class, "usingFlows"); - - verify(mockCompleterClient, times(1)).createFlow(FUNCTION_ID); - } - - @Test - public void onlyOneThreadIsCreatedWhenRuntimeIsAccessedMultipleTimes() { - - when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); - - eventToTestFN().enqueue(); - fnTestHarness.thenRun(FnFlowsFunction.class, "accessRuntimeMultipleTimes"); - - verify(mockCompleterClient, times(1)).createFlow(FUNCTION_ID); - } - -// @Test -// public void invokeWithinAsyncFunction() throws InterruptedException, IOException, ClassNotFoundException { -// -// AtomicReference continuationResult = new AtomicReference<>(); -// CompletionId completionId = new CompletionId("continuation-completion-id"); -// -// when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); -// -// when(mockCompleterClient.supply(eq(FLOW_ID), -// isA(Flows.SerCallable.class),isA(CodeLocation.class))) -// .thenAnswer(invokeContinuation(completionId, continuationResult, "supplyAndGetResult")); -// when(mockCompleterClient.waitForCompletion(eq(FLOW_ID), eq(completionId), eq(getClass().getClassLoader()))) -// .thenAnswer(invocationOnMock -> continuationResult.get()); -// -// httpEventToTestFN().enqueue(); -// fnTestHarness.thenRun(FnFlowsFunction.class, "supplyAndGetResult"); -// -// FnTestHarness.ParsedHttpResponse response = getSingleItem(fnTestHarness.getParsedHttpResponses()); -// assertThat(response.getBodyAsString()).isEqualTo(continuationResult.toString()); -// ArgumentCaptor locCaptor = ArgumentCaptor.forClass(CodeLocation.class); -// verify(mockCompleterClient, times(1)) -// .supply(eq(FLOW_ID), isA(Flows.SerCallable.class), locCaptor.capture()); -// -// CodeLocation gotLocation = locCaptor.getValue(); -// assertThat(gotLocation.getLocation()) -// .matches(Pattern.compile("com\\.fnproject\\.fn\\.runtime\\.testfns\\.FnFlowsFunction\\.supplyAndGetResult\\(.*\\.java\\:\\d+\\)")); -// verify(mockCompleterClient, times(1)) -// .waitForCompletion(eq(FLOW_ID), eq(completionId), eq(getClass().getClassLoader())); -// } -// -// /** -// * Mock the behaviour of a call to the Completer service through supply -// *

-// * When called by Mockito in response to a matching method call, -// * starts a function using the test harness, puts the result into a shared -// * AtomicReference, and returns the supplied Completion Id. -// * -// * @param completionId CompletionId to return from the invocation -// * @param result The result from invoking the continuation -// * @param methodName -// * @return a Mockito Answer instance providing the mock behaviour -// */ -// private Answer invokeContinuation(CompletionId completionId, AtomicReference result, String methodName) { -// return fn -> { -// if (fn.getArguments().length == 3) { -// -// Flows.SerCallable closure = fn.getArgument(1); -// -// -// FnTestHarness fnTestHarness = new FnTestHarness(); -// ` -// fnTestHarness.thenRun(FnFlowsFunction.class, methodName); -// -// FnTestHarness.ParsedHttpResponse response = getInnerResponse(fnTestHarness); -// try { -// assertThat(normalisedHeaders(response)) -// .containsEntry(DATUM_TYPE_HEADER.toLowerCase(), DATUM_TYPE_BLOB) -// .containsEntry(CONTENT_TYPE_HEADER.toLowerCase(), CONTENT_TYPE_JAVA_OBJECT); -// result.set(SerUtils.deserializeObject(response.getBodyAsBytes())); -// } catch (Exception e) { -// result.set(e); -// } -// -// return completionId; -// } else { -// throw new RuntimeException("Too few arguments given to supply"); -// } -// }; -// } -// -// -// -// -// @Test -// public void capturedCallableIsInvoked() throws Exception { -// -// Callable r = (Flows.SerCallable) () -> "Foo Bar"; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(r); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// -// assertThat(getResultObjectFromSingleResponse(fnTestHarness)).isEqualTo("Foo Bar"); -// } -// -// @Test -// public void capturedRunnableIsInvoked() throws Exception { -// Runnable r = (Flows.SerRunnable) () -> { -// tag.set(true); -// }; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(r); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// assertThat(tag.get()).isTrue(); -// } -// -// -// @Test -// public void capturedFunctionWithArgsIsInvoked() throws Exception { -// Function func = (Flows.SerFunction) (in) ->"Foo" + in; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(func) -// .addJavaEntity("BAR"); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// -// assertThat(getResultObjectFromSingleResponse(fnTestHarness)).isEqualTo("FooBAR"); -// } -// -// @Test -// public void catastrophicFailureStillResultsInGraphCommitted() throws Exception { -// when(mockCompleterClient.createFlow(FUNCTION_ID)).thenReturn(FLOW_ID); -// -// httpEventToTestFN().enqueue(); -// fnTestHarness.thenRun(FnFlowsFunction.class, "createFlowAndThenFail"); -// -// verify(mockCompleterClient, times(1)).commit(FLOW_ID); -// } -// -// private Object getResultObjectFromSingleResponse(FnTestHarness fnTestHarness) throws IOException, ClassNotFoundException { -// FnTestHarness.ParsedHttpResponse innerResponse = getInnerResponse(fnTestHarness); -// assertThat(normalisedHeaders(innerResponse)) -// .containsEntry(DATUM_TYPE_HEADER.toLowerCase(), DATUM_TYPE_BLOB) -// .containsEntry(CONTENT_TYPE_HEADER.toLowerCase(), CONTENT_TYPE_JAVA_OBJECT); -// return SerUtils.deserializeObject(innerResponse.getBodyAsBytes()); -// } -// -// private FnTestHarness.ParsedHttpResponse getInnerResponse(FnTestHarness fnTestHarness) { -// FnTestHarness.ParsedHttpResponse response = getSingleItem(fnTestHarness.getParsedHttpResponses()); -// return getSingleItem(FnTestHarness.getParsedHttpResponses(response.getBodyAsBytes())); -// } -// -// private T getSingleItem(List items) { -// assertThat(items.size()).isEqualTo(1); -// return items.get(0); -// } -// -// private Map normalisedHeaders(FnTestHarness.ParsedHttpResponse response) { -// return response.getHeaders().entrySet().stream() -// .collect(Collectors.toMap((kv) -> kv.getKey().toLowerCase(), Map.Entry::getValue)); -// } -// -// @Test -// public void capturedRunnableCanGetCurrentFlowRuntime() throws Exception { -// Callable r = (Flows.SerCallable) () -> { -// return Flows.currentFlow().getClass().getName(); -// }; -// -// TestSerUtils.HttpMultipartSerialization ser = new TestSerUtils.HttpMultipartSerialization() -// .addJavaEntity(r); -// -// httpEventToTestFN() -// .withHeader(FLOW_ID_HEADER, FLOW_ID.getId()) -// .withHeaders(ser.getHeaders()) -// .withBody(ser.getContentStream()) -// .enqueue(); -// -// fnTestHarness.thenRun(FnFlowsFunction.class, "supply"); -// -// assertThat(getResultObjectFromSingleResponse(fnTestHarness)).isEqualTo(RemoteFlow.class.getName()); -// } - - //NotSerializedResult - //Throws Exception in closure - //throws unserialized exception in closure - //null result from closure - //null value to closure. -} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContextTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContextTest.java new file mode 100644 index 00000000..f6b478e3 --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/httpgateway/FunctionHTTPGatewayContextTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.httpgateway; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created on 20/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class FunctionHTTPGatewayContextTest { + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + + @Mock + InvocationContext ctx; + + @Test + public void shouldCreateGatewayContextFromInputs() { + Headers h = Headers.emptyHeaders() + .setHeader("H1", "h1val") + .setHeader("Fn-Http-Method", "PATCH") + .setHeader("Fn-Http-Request-Url", "http://blah.com?a=b&c=d&c=e") + .setHeader("Fn-Http-H-", "ignored") + .setHeader("Fn-Http-H-A", "b") + .setHeader("Fn-Http-H-mv", "c", "d"); + + Mockito.when(ctx.getRequestHeaders()).thenReturn(h); + + FunctionHTTPGatewayContext hctx = new FunctionHTTPGatewayContext(ctx); + + assertThat(hctx.getHeaders()) + .isEqualTo(Headers.emptyHeaders() + .addHeader("A", "b") + .addHeader("mv", "c", "d")); + + + assertThat(hctx.getRequestURL()) + .isEqualTo("http://blah.com?a=b&c=d&c=e"); + + assertThat(hctx.getQueryParameters().get("a")).contains("b"); + assertThat(hctx.getQueryParameters().getValues("c")).contains("d", "e"); + + assertThat(hctx.getMethod()).isEqualTo("PATCH"); + + + } + + + @Test + public void shouldCreateGatewayContextFromEmptyHeaders() { + + Mockito.when(ctx.getRequestHeaders()).thenReturn(Headers.emptyHeaders()); + + FunctionHTTPGatewayContext hctx = new FunctionHTTPGatewayContext(ctx); + + assertThat(hctx.getMethod()).isEqualTo(""); + assertThat(hctx.getRequestURL()).isEqualTo(""); + assertThat(hctx.getHeaders()).isEqualTo(Headers.emptyHeaders()); + assertThat(hctx.getQueryParameters().getAll()).isEmpty(); + + } + + + @Test + public void shouldPassThroughResponseAttributes() { + + Mockito.when(ctx.getRequestHeaders()).thenReturn(Headers.emptyHeaders()); + + FunctionHTTPGatewayContext hctx = new FunctionHTTPGatewayContext(ctx); + hctx.setResponseHeader("My-Header", "foo", "bar"); + Mockito.verify(ctx).setResponseHeader("Fn-Http-H-My-Header", "foo", "bar"); + + hctx.setResponseHeader("Content-Type", "my/ct", "ignored"); + Mockito.verify(ctx).setResponseContentType("my/ct"); + Mockito.verify(ctx).setResponseHeader("Fn-Http-H-Content-Type", "my/ct"); + + hctx.addResponseHeader("new-H", "v1"); + Mockito.verify(ctx).addResponseHeader("Fn-Http-H-new-H", "v1"); + + hctx.setStatusCode(101); + + + Mockito.verify(ctx).setResponseHeader("Fn-Http-Status", "101"); + + } + +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketNativeTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketNativeTest.java new file mode 100644 index 00000000..228af289 --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketNativeTest.java @@ -0,0 +1,486 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.ntv; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixSocketNativeTest { + + @BeforeClass + public static void init() { + if (System.getenv("RUNTIME_BUILD_DIR") == null) { + System.setProperty("com.fnproject.java.native.libdir", new File("src/main/c/").getAbsolutePath()); + }else{ + System.setProperty("com.fnproject.java.native.libdir", new File(System.getenv("RUNTIME_BUILD_DIR")).getAbsolutePath()); + } + } + + File createSocketFile() throws IOException { + File f = File.createTempFile("socket", "sock"); + f.delete(); + f.deleteOnExit(); + return f; + } + + @Test + public void shouldHandleBind() throws Exception { + + + try { // invalid socket + UnixSocketNative.bind(-1, createSocketFile().getAbsolutePath()); + fail("should have thrown an invalid argument"); + } catch (UnixSocketException ignored) { + } + + int socket = UnixSocketNative.socket(); + try { // invalid file location + UnixSocketNative.bind(socket, "/tmp/foodir/socket"); + fail("should have thrown an invalid argument"); + } catch (UnixSocketException ignored) { + } finally { + UnixSocketNative.close(socket); + } + + + socket = UnixSocketNative.socket(); + File socketFile = createSocketFile(); + try { // valid bind + UnixSocketNative.bind(socket, socketFile.getAbsolutePath()); + } finally { + UnixSocketNative.close(socket); + } + } + + public CompletableFuture runServerLoop(Callable loop) { + CompletableFuture result = new CompletableFuture<>(); + Thread t = new Thread(() -> { + try { + result.complete(loop.call()); + } catch (Exception e) { + result.completeExceptionally(e); + } + }); + t.start(); + return result; + } + + @Test + public void shouldHandleConnectAccept() throws Exception { + + // invalid socket + { + try { + UnixSocketNative.connect(-1, "/tmp/nonexistant_path.sock"); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + } + + + // unknown path + { + int socket = UnixSocketNative.socket(); + try { + UnixSocketNative.connect(socket, "/tmp/nonexistant_path.sock"); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } finally { + UnixSocketNative.close(socket); + } + } + // accept rejects sresult = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + return cs > 0; + } finally { + UnixSocketNative.close(ss); + } + + }); + ready.await(); + int cs; + cs = UnixSocketNative.socket(); + UnixSocketNative.connect(cs, serverSocket.getAbsolutePath()); + + assertThat(sresult.get()).isTrue(); + } + + } + + @Test + public void shouldHonorWrites() throws Exception { + + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + byte[] buf = new byte[100]; + int read = UnixSocketNative.recv(cs, buf, 0, buf.length); + byte[] newBuf = new byte[read]; + System.arraycopy(buf, 0, newBuf, 0, read); + + return newBuf; + } finally { + UnixSocketNative.close(ss); + } + }); + + + {// zero byte write is a noop + ready.await(); + int cs = UnixSocketNative.socket(); + UnixSocketNative.connect(cs, serverSocket.getAbsolutePath()); + + // must NPE on buff + try { + UnixSocketNative.send(cs, null, 0, 10); + fail("should have NPEd"); + } catch (NullPointerException ignored) { + + } + + + // invalid offset + try { + UnixSocketNative.send(cs, "hello".getBytes(), 100, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.send(cs, "hello".getBytes(), -1, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.send(cs, "hello".getBytes(), 0, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + try { + // Must nop on write + UnixSocketNative.send(cs, new byte[0], 0, 0); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // validate a real write to be sure + UnixSocketNative.send(cs, "hello".getBytes(), 0, 5); + byte[] got = result.get(); + + assertThat(got).isEqualTo("hello".getBytes()); + } + } + + + @Test + public void shouldHonorReads() throws Exception { + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + int read = UnixSocketNative.send(cs, "hello".getBytes(), 0, 5); + UnixSocketNative.close(cs); + return true; + } finally { + UnixSocketNative.close(ss); + } + }); + + + {// zero byte write is a noop + ready.await(); + int cs = UnixSocketNative.socket(); + UnixSocketNative.connect(cs, serverSocket.getAbsolutePath()); + + // must NPE on buff + try { + UnixSocketNative.recv(cs, null, 0, 10); + fail("should have NPEd"); + } catch (NullPointerException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.recv(cs, new byte[5], -1, 1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + // invalid length + try { + UnixSocketNative.recv(cs, new byte[5], 0, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid length + try { + UnixSocketNative.recv(cs, new byte[5], 0, 0); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset beyond buffer + try { + UnixSocketNative.recv(cs, new byte[5], 100, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // invalid offset + try { + UnixSocketNative.recv(cs, "hello".getBytes(), -1, 10); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + + } + + // validate a real write to be sure + byte[] buf = new byte[5]; + + int count = UnixSocketNative.recv(cs, buf, 0, 5); + assertThat(count).isEqualTo(5); + + assertThat(buf).isEqualTo("hello".getBytes()); + } + } + + @Test + public void shouldSetSocketOpts() throws Exception { + + int sock = UnixSocketNative.socket(); + try { + try { + UnixSocketNative.setSendBufSize(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setSendBufSize(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setSendBufSize(sock, 65535); + + + try { + UnixSocketNative.setRecvBufSize(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setRecvBufSize(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setRecvBufSize(sock, 65535); + + + try { + UnixSocketNative.setSendTimeout(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setSendTimeout(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setSendTimeout(sock, 2000); + assertThat(UnixSocketNative.getSendTimeout(sock)).isEqualTo(2000); + + + try { + UnixSocketNative.setRecvTimeout(-1, 1); + fail("should have failed"); + } catch (UnixSocketException ignored) { + } + + try { + UnixSocketNative.setRecvTimeout(sock, -1); + fail("should have IAEd"); + } catch (IllegalArgumentException ignored) { + } + + UnixSocketNative.setRecvTimeout(sock, 3000); + assertThat(UnixSocketNative.getRecvTimeout(sock)).isEqualTo(3000); + + + } finally { + UnixSocketNative.close(sock); + } + + } + + + @Test + public void shouldHandleReadTimeouts() throws Exception { + + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + int cs = UnixSocketNative.accept(ss, 0); + Thread.sleep(100); + int read = UnixSocketNative.send(cs, "hello".getBytes(), 0, 5); + UnixSocketNative.close(cs); + return true; + } finally { + UnixSocketNative.close(ss); + } + }); + + int clientFd = UnixSocketNative.socket(); + UnixSocketNative.setRecvTimeout(clientFd,50); + + ready.await(); + UnixSocketNative.connect(clientFd,serverSocket.getAbsolutePath()); + byte[] buf = new byte[100]; + try { + UnixSocketNative.recv(clientFd, buf, 0, buf.length); + fail("should have timed out"); + }catch (SocketTimeoutException ignored){ + } + + } + + + @Test + public void shouldHandleConnectTimeouts() throws Exception { + + CountDownLatch ready = new CountDownLatch(1); + File serverSocket = createSocketFile(); + + CompletableFuture result = runServerLoop(() -> { + int ss = UnixSocketNative.socket(); + try { + UnixSocketNative.bind(ss, serverSocket.getAbsolutePath()); + UnixSocketNative.listen(ss, 1); + ready.countDown(); + Thread.sleep(1000); + + return true; + } finally { + UnixSocketNative.close(ss); + } + }); + + int clientFd = UnixSocketNative.socket(); + UnixSocketNative.setSendTimeout(clientFd,50); + ready.await(); + try { + UnixSocketNative.connect(clientFd,serverSocket.getAbsolutePath()); + + }catch (SocketTimeoutException ignored){ + } + + } + +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketTest.java new file mode 100644 index 00000000..1e8dc233 --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/ntv/UnixSocketTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.ntv; + +import org.assertj.core.api.Assertions; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.DataInputStream; +import java.io.File; +import java.io.IOException; +import java.util.Random; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; + +/** + * Created on 12/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public class UnixSocketTest { + + @BeforeClass + public static void setup() { + if (System.getenv("RUNTIME_BUILD_DIR") == null) { + System.setProperty("com.fnproject.java.native.libdir", new File("src/main/c/").getAbsolutePath()); + }else{ + System.setProperty("com.fnproject.java.native.libdir", new File(System.getenv("RUNTIME_BUILD_DIR")).getAbsolutePath()); + } + } + + File createSocketFile() throws IOException { + File f = File.createTempFile("socket", "sock"); + f.delete(); + f.deleteOnExit(); + return f; + } + + public byte[] roundTripViaEcho(byte[] data) throws Exception { + + File f = createSocketFile(); + try (UnixServerSocket ss = UnixServerSocket.listen(f.getPath(), 1)) { + + CompletableFuture result = new CompletableFuture<>(); + CountDownLatch cdl = new CountDownLatch(1); + Thread client = new Thread(() -> { + try { + cdl.await(); + try (UnixSocket us = UnixSocket.connect(f.getPath())) { + us.setReceiveBufferSize(65535); + us.setSendBufferSize(65535); + byte[] buf = new byte[data.length]; + us.getOutputStream().write(data); + DataInputStream dis = new DataInputStream(us.getInputStream()); + dis.readFully(buf); + result.complete(buf); + } + } catch (Exception e) { + result.completeExceptionally(e); + } + }); + client.start(); + + cdl.countDown(); + UnixSocket in = ss.accept(1000); + byte[] sbuf = new byte[data.length]; + in.setReceiveBufferSize(65535); + in.setSendBufferSize(65535); + new DataInputStream(in.getInputStream()).readFully(sbuf); + in.getOutputStream().write(sbuf); + in.close(); + return result.get(); + } + } + + @Test + public void shouldHandleEmptyData() throws Exception { + byte[] data = "hello".getBytes(); + Assertions.assertThat(roundTripViaEcho(data)).isEqualTo(data); + + } + + + @Test + public void shouldHandleBigData() throws Exception { + Random r = new Random(); + byte[] dataPart = new byte[2048]; + + r.nextBytes(dataPart); + + byte[] data = new byte[1024 * 1024 * 10]; + for (int i = 0 ; i < data.length ;i += dataPart.length){ + System.arraycopy(dataPart,0,data,i,dataPart.length); + } + + Assertions.assertThat(roundTripViaEcho(data)).isEqualTo(data); + + } +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/Animal.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/Animal.java index 1760598e..38448c02 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/Animal.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/Animal.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/BadTestFnDuplicateMethods.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/BadTestFnDuplicateMethods.java index dd883e52..98a53e4a 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/BadTestFnDuplicateMethods.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/BadTestFnDuplicateMethods.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; /** diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java index b4df6eb0..260aab80 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnInputOutput.java @@ -1,9 +1,25 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; +import com.fnproject.fn.api.FnConfiguration; import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; -import com.fnproject.fn.api.FnConfiguration; public class CustomDataBindingFnInputOutput { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java index 990642a3..362984c2 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotation.java @@ -1,8 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.InputBinding; - - import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; public class CustomDataBindingFnWithAnnotation { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java index a85d9c91..151eb18f 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithAnnotationAndConfig.java @@ -1,9 +1,24 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; import com.fnproject.fn.api.InputBinding; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java index 7c5b1e75..f1d70df5 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithConfig.java @@ -1,7 +1,22 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.FnConfiguration; - import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java index 1ccf8281..a6a40a1d 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithDudCoercion.java @@ -1,8 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.FnConfiguration; import com.fnproject.fn.api.RuntimeContext; - import com.fnproject.fn.runtime.testfns.coercions.DudCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java index 9b881a88..1da6b54f 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithMultipleCoercions.java @@ -1,8 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoercions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoercions.java new file mode 100644 index 00000000..f6f7b3a6 --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoercions.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.runtime.testfns; + +import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; + +public class CustomDataBindingFnWithNoUserCoercions { + + @FnConfiguration + public static void inputConfig(RuntimeContext ctx){ + } + + public String echo(String s){ + return s; + } +} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoersions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoersions.java deleted file mode 100644 index 9dad4a3d..00000000 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomDataBindingFnWithNoUserCoersions.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.fnproject.fn.runtime.testfns; - -import com.fnproject.fn.api.RuntimeContext; -import com.fnproject.fn.api.FnConfiguration; - -public class CustomDataBindingFnWithNoUserCoersions { - - @FnConfiguration - public static void inputConfig(RuntimeContext ctx){ - } - - public String echo(String s){ - return s; - } -} diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java index 089b461d..19630a68 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithAnnotation.java @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.api.OutputBinding; +import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; public class CustomOutputDataBindingFnWithAnnotation { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java index ce8dbc75..d35d8032 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithConfig.java @@ -1,8 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java index b4963004..e3a91da9 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithDudCoercion.java @@ -1,8 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; - +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.DudCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java index 9f194366..acd92e88 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithMultipleCoercions.java @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.runtime.testfns.coercions.StringReversalCoercion; import com.fnproject.fn.runtime.testfns.coercions.StringUpperCaseCoercion; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java index c62934d1..1048ee4a 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/CustomOutputDataBindingFnWithNoUserCoercions.java @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; public class CustomOutputDataBindingFnWithNoUserCoercions { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/ErrorMessages.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/ErrorMessages.java index 1ed20e30..23371c5a 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/ErrorMessages.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/ErrorMessages.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; public class ErrorMessages { diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java index 345262a5..abe6635e 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFn.java @@ -1,6 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.InvocationContext; import org.apache.commons.io.IOUtils; import java.io.IOException; @@ -105,6 +122,21 @@ public static String readSecondInput(InputEvent evt) { } + + public static void readRawEvent(InputEvent evt) { + input = evt; + + } + + + public static void setsOutputHeaders(InvocationContext ic) { + ic.addResponseHeader("Header-1","v1"); + ic.setResponseContentType("foo-ct"); + ic.addResponseHeader("a","b1"); + ic.addResponseHeader("a","b2"); + + + } public static List fnGenericAnimal() { Animal dog = new Animal("Spot", 6); Animal cat = new Animal("Jason", 16); @@ -115,7 +147,7 @@ public static List fnGenericAnimal() { * Reset the internal (static) state * Should be called between runs; */ - public static final void reset() { + public static void reset() { input = NOTHING; output = NOTHING; count = 0; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnConstructors.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnConstructors.java index 73f41d44..ba9e6433 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnConstructors.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnConstructors.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; import com.fnproject.fn.api.RuntimeContext; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java index 3c69ac64..9fc0a22e 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/TestFnWithConfigurationMethods.java @@ -1,7 +1,23 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns; -import com.fnproject.fn.api.RuntimeContext; import com.fnproject.fn.api.FnConfiguration; +import com.fnproject.fn.api.RuntimeContext; import java.util.Map; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/DudCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/DudCoercion.java index af9d3547..b6c68e8a 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/DudCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/DudCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns.coercions; import com.fnproject.fn.api.*; diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java index a08d6bb7..67e37d37 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringReversalCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns.coercions; import com.fnproject.fn.api.*; @@ -26,7 +42,7 @@ public Optional wrapFunctionResult(InvocationContext ctx, MethodWra if (ctx.getRuntimeContext().getMethod().getTargetMethod().getReturnType().equals(String.class)) { try { String reversedOutput = new StringBuffer((String) value).reverse().toString(); - return Optional.of(OutputEvent.fromBytes(reversedOutput.getBytes(), OutputEvent.SUCCESS, "text/plain")); + return Optional.of(OutputEvent.fromBytes(reversedOutput.getBytes(), OutputEvent.Status.Success, "text/plain")); } catch (ClassCastException e) { return Optional.empty(); } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java index b2a5959e..7afcf314 100644 --- a/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java +++ b/runtime/src/test/java/com/fnproject/fn/runtime/testfns/coercions/StringUpperCaseCoercion.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.runtime.testfns.coercions; import com.fnproject.fn.api.*; @@ -14,7 +30,7 @@ public Optional tryCoerceParam(InvocationContext currentContext, int arg return Optional.of( input.consumeBody(is -> { try { - return new StringBuffer(IOUtils.toString(is, StandardCharsets.UTF_8)).toString().toUpperCase(); + return IOUtils.toString(is, StandardCharsets.UTF_8).toUpperCase(); } catch (IOException e) { return null; // Tests will fail if we end up here } @@ -26,8 +42,8 @@ public Optional tryCoerceParam(InvocationContext currentContext, int arg public Optional wrapFunctionResult(InvocationContext ctx, MethodWrapper method, Object value) { if (ctx.getRuntimeContext().getMethod().getTargetMethod().getReturnType().equals(String.class)) { try { - String capitilisedOutput = new StringBuffer((String) value).toString().toUpperCase(); - return Optional.of(OutputEvent.fromBytes(capitilisedOutput.getBytes(), OutputEvent.SUCCESS, "text/plain")); + String capitalizedOutput = ((String) value).toUpperCase(); + return Optional.of(OutputEvent.fromBytes(capitalizedOutput.getBytes(), OutputEvent.Status.Success, "text/plain")); } catch (ClassCastException e) { return Optional.empty(); } diff --git a/runtime/src/test/java/com/fnproject/fn/runtime/tracing/OCITracingContextTest.java b/runtime/src/test/java/com/fnproject/fn/runtime/tracing/OCITracingContextTest.java new file mode 100644 index 00000000..68c2712f --- /dev/null +++ b/runtime/src/test/java/com/fnproject/fn/runtime/tracing/OCITracingContextTest.java @@ -0,0 +1,110 @@ +package com.fnproject.fn.runtime.tracing; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InvocationContext; +import com.fnproject.fn.api.MethodWrapper; +import com.fnproject.fn.runtime.FunctionRuntimeContext; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OCITracingContextTest { + + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Mock + InvocationContext ctxMock; + + @Mock + FunctionRuntimeContext runtimeContextMock; + + @Mock + MethodWrapper methodWrapperMock; + + private Map getConfig(Boolean enabled) { + Map env = new HashMap<>(); + env.put("FN_APP_NAME", "myapp"); + env.put("FN_FN_NAME", "myFunction"); + env.put("OCI_TRACE_COLLECTOR_URL", "tracingPath"); + env.put("OCI_TRACING_ENABLED", enabled ? "1" : "0"); + return Collections.unmodifiableMap(env); + } + + @Test + public void configureRuntimeContext() { + Map config = getConfig(false); + runtimeContextMock = new FunctionRuntimeContext(methodWrapperMock, config); + + OCITracingContext tracingContext = new OCITracingContext(ctxMock, runtimeContextMock); + assertThat(tracingContext.isTracingEnabled()).isEqualTo(false); + assertThat(tracingContext.getAppName()).isEqualToIgnoringCase("myapp"); + assertThat(tracingContext.getFunctionName()).isEqualToIgnoringCase("myFunction"); + assertThat(tracingContext.getTraceCollectorURL()).isEqualToIgnoringCase("tracingPath"); + } + + @Test + public void getServiceName() { + Map config = getConfig(false); + runtimeContextMock = new FunctionRuntimeContext(methodWrapperMock, config); + + OCITracingContext tracingContext = new OCITracingContext(ctxMock, runtimeContextMock); + assertThat(tracingContext.getServiceName()).isEqualToIgnoringCase("myapp::myFunction"); + } + + @Test + public void shouldAbleToConfigureWithNoHeaderData() { + Map config = getConfig(true); + runtimeContextMock = new FunctionRuntimeContext(methodWrapperMock, config); + Mockito.when(ctxMock.getRequestHeaders()).thenReturn(Headers.emptyHeaders()); + + OCITracingContext tracingContext = new OCITracingContext(ctxMock, runtimeContextMock); + assertThat(tracingContext.isSampled()).isEqualTo(true); + assertThat(tracingContext.getTraceId()).isEqualTo("1"); + assertThat(tracingContext.getSpanId()).isEqualTo("1"); + assertThat(tracingContext.getParentSpanId()).isEqualTo("1"); + } + + @Test + public void shouldAbleToConfigureWithHeaderDataNotSampled() { + Map config = getConfig(true); + runtimeContextMock = new FunctionRuntimeContext(methodWrapperMock, config); + Map headerData = new HashMap(); + headerData.put("x-b3-sampled","0"); + Headers headers = Headers.fromMap(headerData); + Mockito.when(ctxMock.getRequestHeaders()).thenReturn(headers); + + OCITracingContext tracingContext = new OCITracingContext(ctxMock, runtimeContextMock); + assertThat(tracingContext.isSampled()).isEqualTo(false); + } + + @Test + public void shouldAbleToConfigureWithHeaderData() { + Map config = getConfig(true); + runtimeContextMock = new FunctionRuntimeContext(methodWrapperMock, config); + Map headerData = new HashMap(); + headerData.put("x-b3-sampled","1"); + headerData.put("x-b3-flags",""); + headerData.put("x-b3-traceid","213454321432"); + headerData.put("x-b3-spanid","244342r343"); + headerData.put("x-b3-parentspanid","32142r231242"); + Headers headers = Headers.fromMap(headerData); + Mockito.when(ctxMock.getRequestHeaders()).thenReturn(headers); + + OCITracingContext tracingContext = new OCITracingContext(ctxMock, runtimeContextMock); + assertThat(tracingContext.isSampled()).isEqualTo(true); + assertThat(tracingContext.getTraceId()).isEqualTo("213454321432"); + assertThat(tracingContext.getSpanId()).isEqualTo("244342r343"); + assertThat(tracingContext.getParentSpanId()).isEqualTo("32142r231242"); + assertThat(tracingContext.getFlags()).isEqualTo(""); + } +} diff --git a/runtime/src/test/java/not/in/com/fnproject/fn/StacktraceFilteringTestFunctions.java b/runtime/src/test/java/not/in/com/fnproject/fn/StacktraceFilteringTestFunctions.java index 9a6620f4..daf95794 100644 --- a/runtime/src/test/java/not/in/com/fnproject/fn/StacktraceFilteringTestFunctions.java +++ b/runtime/src/test/java/not/in/com/fnproject/fn/StacktraceFilteringTestFunctions.java @@ -1,3 +1,19 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package not.in.com.fnproject.fn; import com.fnproject.fn.api.FnConfiguration; diff --git a/settings-deploy.xml b/settings-deploy.xml index ee487f49..833ea868 100644 --- a/settings-deploy.xml +++ b/settings-deploy.xml @@ -1,3 +1,21 @@ + + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + testing-core + testing-core + + + + com.fnproject.fn + runtime + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java similarity index 54% rename from testing/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java rename to testing-core/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java index 1dce2476..61cb3725 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnEventBuilder.java @@ -1,11 +1,28 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.testing; +import java.io.IOException; import java.io.InputStream; /** * Builder for function input events */ -public interface FnEventBuilder { +public interface FnEventBuilder { /** * Add a header to the input with a variable number of values; duplicate headers will be overwritten @@ -14,7 +31,7 @@ public interface FnEventBuilder { * @param value header value(s) * @return an event builder */ - FnEventBuilder withHeader(String key, String value); + FnEventBuilder withHeader(String key, String value); /** * Set the body of the request by providing an InputStream @@ -22,10 +39,9 @@ public interface FnEventBuilder { * Note - setting the body to an input stream means that only one event can be enqueued using this builder. * * @param body the bytes of the body - * @param contentLength how long the body is supposed to be * @return an event builder */ - FnEventBuilder withBody(InputStream body, int contentLength); + FnEventBuilder withBody(InputStream body) throws IOException; /** * Set the body of the request as a byte array @@ -33,7 +49,7 @@ public interface FnEventBuilder { * @param body the bytes of the body * @return an event builder */ - FnEventBuilder withBody(byte[] body); + FnEventBuilder withBody(byte[] body); /** * Set the body of the request as a String @@ -41,48 +57,7 @@ public interface FnEventBuilder { * @param body the String of the body * @return an event builder */ - FnEventBuilder withBody(String body); - - /** - * Set the app name associated with the call - * - * @param appName the app name - * @return an event builder - */ - FnEventBuilder withAppName(String appName); - - /** - * Set the fn route associated with the call - * - * @param route the route - * @return an event builder - */ - FnEventBuilder withRoute(String route); - - /** - * Set the HTTP method of the incoming request - * - * @param method an HTTP method - * @return an event builder - */ - FnEventBuilder withMethod(String method); - - /** - * Set the request URL of the incoming event - * - * @param requestUrl the request URL - * @return an event builder - */ - FnEventBuilder withRequestUrl(String requestUrl); - - /** - * Add a query parameter to the request URL - * - * @param key - non URL encoded key - * @param value - non URL encoded value - * @return an event builder - */ - FnEventBuilder withQueryParameter(String key, String value); + FnEventBuilder withBody(String body); /** * Consume the builder and enqueue this event to be passed into the function when it is run @@ -90,7 +65,8 @@ public interface FnEventBuilder { * @return The original testing rule. The builder is consumed. * @throws IllegalStateException if this event has already been enqueued and the event input can only be read once. */ - FnTestingRule enqueue(); +// FnTestingRule enqueue(); + T enqueue(); /** * Consume the builder and enqueue multiple copies of this event. @@ -102,5 +78,6 @@ public interface FnEventBuilder { * @return The original testing rule. The builder is consumed. * @throws IllegalStateException if the body cannot be read multiple times. */ - FnTestingRule enqueue(int n); +// FnTestingRule enqueue(int n); + T enqueue(int n); } diff --git a/testing-core/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java new file mode 100644 index 00000000..75762da0 --- /dev/null +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.runtime.ReadOnceInputEvent; +import org.apache.commons.io.IOUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Map; +import java.util.Objects; + +public class FnHttpEventBuilder { + private byte[] bodyBytes = new byte[0]; + private Headers headers = Headers.emptyHeaders(); + private Instant deadline = Instant.now().plus(1, ChronoUnit.HOURS); + + public FnHttpEventBuilder withHeader(String key, String value) { + Objects.requireNonNull(key, "key"); + Objects.requireNonNull(value, "value"); + headers = headers.addHeader(key, value); + return this; + } + + public FnHttpEventBuilder withBody(InputStream body) throws IOException { + Objects.requireNonNull(body, "body"); + this.bodyBytes = IOUtils.toByteArray(body); + + return this; + } + + public FnHttpEventBuilder withBody(byte[] body) { + Objects.requireNonNull(body, "body"); + this.bodyBytes = body; + + return this; + } + + public FnHttpEventBuilder withBody(String body) { + byte stringAsBytes[] = Objects.requireNonNull(body, "body").getBytes(); + return withBody(stringAsBytes); + } + + + public FnHttpEventBuilder withHeaders(Map headers) { + Headers h = this.headers; + for (Map.Entry he : headers.entrySet()) { + h = h.setHeader(he.getKey(), he.getValue()); + } + this.headers = h; + return this; + } + + + public InputEvent buildEvent() { + return new ReadOnceInputEvent(new ByteArrayInputStream(bodyBytes), headers, "callId", deadline); + } + + +} diff --git a/testing-core/src/main/java/com/fnproject/fn/testing/FnResult.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnResult.java new file mode 100644 index 00000000..814067a3 --- /dev/null +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnResult.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +import com.fnproject.fn.api.OutputEvent; + +/** + * A simple abstraction over {@link OutputEvent} that buffers the response body + */ +public interface FnResult extends OutputEvent { + /** + * Returns the body of the function result as a byte array + * + * @return the function response body + */ + byte[] getBodyAsBytes(); + + /** + * Returns the body of the function response as a string + * + * @return a function response body + */ + String getBodyAsString(); + + + /** + * Determine if the status code corresponds to a successful invocation + * + * @return true if the status code indicates success + */ + default boolean isSuccess() { + return getStatus() == Status.Success; + } +} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java b/testing-core/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java similarity index 65% rename from testing/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java rename to testing-core/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java index ba3dc085..af0ea446 100644 --- a/testing/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FnTestingClassLoader.java @@ -1,14 +1,27 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.fnproject.fn.testing; import com.fnproject.fn.runtime.EntryPoint; -import com.fnproject.fn.runtime.flow.CompleterClient; -import com.fnproject.fn.runtime.flow.CompleterClientFactory; -import com.fnproject.fn.runtime.flow.FlowRuntimeGlobals; +import com.fnproject.fn.runtime.EventCodec; import org.apache.commons.io.IOUtils; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -19,11 +32,11 @@ /** * Testing classloader that loads all classes afresh when needed, otherwise delegates shared classes to the parent classloader */ -class FnTestingClassLoader extends ClassLoader { +public class FnTestingClassLoader extends ClassLoader { private final List sharedPrefixes; private final Map> loaded = new HashMap<>(); - FnTestingClassLoader(ClassLoader parent, List sharedPrefixes) { + public FnTestingClassLoader(ClassLoader parent, List sharedPrefixes) { super(parent); this.sharedPrefixes = sharedPrefixes; } @@ -68,16 +81,8 @@ public synchronized Class loadClass(String className) throws ClassNotFoundExc return cls; } - void setCompleterClient(CompleterClientFactory completerClientFactory) { - try { - Class completerGlobals = loadClass(FlowRuntimeGlobals.class.getName()); - callMethodInFnClassloader(completerGlobals, "setCompleterClientFactory", CompleterClientFactory.class).invoke(completerGlobals, completerClientFactory); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | ClassNotFoundException | IllegalArgumentException e) { - throw new RuntimeException("Something broke in the reflective classloader", e); - } - } - public int run(Map mutableEnv, InputStream is, PrintStream functionOut, PrintStream functionErr, String... s) { + public int run(Map mutableEnv, EventCodec codec, PrintStream functionErr, String... s) { ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader(); try { @@ -87,16 +92,16 @@ public int run(Map mutableEnv, InputStream is, PrintStream funct Class entryPoint_class = loadClass(EntryPoint.class.getName()); Object entryPoint = entryPoint_class.newInstance(); - return (int) callMethodInFnClassloader(entryPoint, "run", Map.class, InputStream.class, OutputStream.class, PrintStream.class, String[].class) - .invoke(entryPoint, mutableEnv, is, functionOut, functionErr, s); + return (int) getMethodInClassLoader(entryPoint, "run", Map.class, EventCodec.class, PrintStream.class, String[].class) + .invoke(entryPoint, mutableEnv, codec, functionErr, s); } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException | IllegalArgumentException e) { throw new RuntimeException("Something broke in the reflective classloader", e); - }finally { + } finally { Thread.currentThread().setContextClassLoader(currentClassLoader); } } - private Method callMethodInFnClassloader(Object target, String method, Class... types) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + private Method getMethodInClassLoader(Object target, String method, Class... types) throws NoSuchMethodException { Class targetClass; if (target instanceof Class) { targetClass = (Class) target; diff --git a/testing-core/src/main/java/com/fnproject/fn/testing/FunctionError.java b/testing-core/src/main/java/com/fnproject/fn/testing/FunctionError.java new file mode 100644 index 00000000..5cf5cbc9 --- /dev/null +++ b/testing-core/src/main/java/com/fnproject/fn/testing/FunctionError.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +/** + * An Exception that can be used in invocations of stubbed external functions to signal the failure of the external + * function due to a simulated error case of the function itself + */ +public class FunctionError extends Exception { + public FunctionError() { + } + + public FunctionError(String s) { + super(s); + } +} diff --git a/testing-core/src/main/java/com/fnproject/fn/testing/HeaderWriter.java b/testing-core/src/main/java/com/fnproject/fn/testing/HeaderWriter.java new file mode 100644 index 00000000..7ef2f8b8 --- /dev/null +++ b/testing-core/src/main/java/com/fnproject/fn/testing/HeaderWriter.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +import java.io.IOException; +import java.io.OutputStream; + +class HeaderWriter { + final OutputStream os; + + HeaderWriter(OutputStream os) { + this.os = os; + } + + void writeHeader(String key, String value) throws IOException { + os.write((key + ": " + value + "\r\n").getBytes("ISO-8859-1")); + } +} diff --git a/testing-core/src/main/java/com/fnproject/fn/testing/PlatformError.java b/testing-core/src/main/java/com/fnproject/fn/testing/PlatformError.java new file mode 100644 index 00000000..2255cd74 --- /dev/null +++ b/testing-core/src/main/java/com/fnproject/fn/testing/PlatformError.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +/** + * An Exception that can be used in invocations of stubbed external functions to signal the failure of the external + * function due to a simulated infrastructure error in the Oracle Functions platform + */ +public class PlatformError extends Exception { + public PlatformError() { + } + + public PlatformError(String s) { + super(s); + } +} diff --git a/testing-junit4/pom.xml b/testing-junit4/pom.xml new file mode 100644 index 00000000..b1089a9c --- /dev/null +++ b/testing-junit4/pom.xml @@ -0,0 +1,71 @@ + + + + + + fdk + com.fnproject.fn + 1.0.0-SNAPSHOT + + 4.0.0 + testing-junit4 + testing-junit4 + + + + com.fnproject.fn + testing-core + + + com.fnproject.fn + runtime + + + + junit + junit + compile + + + org.assertj + assertj-core + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + + + diff --git a/testing-junit4/src/main/java/com/fnproject/fn/testing/FnEventBuilderJUnit4.java b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnEventBuilderJUnit4.java new file mode 100644 index 00000000..343d97f2 --- /dev/null +++ b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnEventBuilderJUnit4.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +public interface FnEventBuilderJUnit4 extends FnEventBuilder { + +} diff --git a/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRule.java b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRule.java new file mode 100644 index 00000000..55f65fc0 --- /dev/null +++ b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRule.java @@ -0,0 +1,464 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fnproject.fn.api.*; +import com.fnproject.fn.runtime.EventCodec; +import org.apache.commons.io.output.TeeOutputStream; +import org.junit.Rule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.io.*; +import java.util.*; + + +/** + * Testing {@link Rule} for fn Java FDK functions. + *

+ * This interface facilitates: + *

    + *
  • The creation of an in-memory environment replicating the functionality of the {@code fn} service
  • + *
  • The creation of input events passed to a user function using {@link #givenEvent()}
  • + *
  • The verification of function behaviour by accessing output represented by {@link FnResult} instances.
  • + *
+ *

Example Usage:

+ *
{@code
+ * public class MyFunctionTest {
+ *     {@literal @}Rule
+ *     public final FnTestingRule testing = FnTestingRule.createDefault();
+ *
+ *     {@literal @}Test
+ *     public void myTest() {
+ *         // Create an event to invoke MyFunction and put it into the event queue
+ *         fn.givenEvent()
+ *            .addHeader("FOO", "BAR")
+ *            .withBody("Body")
+ *            .enqueue();
+ *
+ *         // Run MyFunction#handleRequest using the built event queue from above
+ *         fn.thenRun(MyFunction.class, "handleRequest");
+ *
+ *         // Get the function result and check it for correctness
+ *         FnResult result = fn.getOnlyResult();
+ *         assertThat(result.getStatus()).isEqualTo(200);
+ *         assertThat(result.getBodyAsString()).isEqualTo("expected return value of my function");
+ *     }
+ * }}
+ */ +public final class FnTestingRule implements TestRule { + private final Map config = new HashMap<>(); + private Map eventEnv = new HashMap<>(); + private boolean hasEvents = false; + private List pendingInput = Collections.synchronizedList(new ArrayList<>()); + private List output = Collections.synchronizedList(new ArrayList<>()); + private ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + + private static final ObjectMapper objectMapper = new ObjectMapper(); + private final List sharedPrefixes = new ArrayList<>(); + private int lastExitCode; + private final List features = new ArrayList<>(); + + { + // Internal shared classes required to bridge completer into tests + addSharedClassPrefix("java."); + addSharedClassPrefix("javax."); + addSharedClassPrefix("sun."); + addSharedClassPrefix("jdk."); + + + addSharedClass(Headers.class); + addSharedClass(InputEvent.class); + addSharedClass(OutputEvent.class); + addSharedClass(OutputEvent.Status.class); + addSharedClass(TestOutput.class); + addSharedClass(TestCodec.class); + addSharedClass(EventCodec.class); + addSharedClass(EventCodec.Handler.class); + + + + } + + /** + * TestOutput represents an output of a function it wraps OutputEvent and provides buffered access to the function output + */ + public static final class TestOutput implements FnResult { + private final OutputEvent from; + byte[] body; + + private TestOutput(OutputEvent from) throws IOException { + this.from = Objects.requireNonNull(from, "from"); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + from.writeToOutput(bos); + body = bos.toByteArray(); + } + + /** + * construct a test output from an output event - this consums the body of the output event + * + * @param from an output event to consume + * @return a new TestEvent that wraps the passed even t + * @throws IOException + */ + public static TestOutput fromOutputEvent(OutputEvent from) throws IOException { + return new TestOutput(from); + } + + @Override + public Status getStatus() { + return from.getStatus(); + } + + @Override + public Optional getContentType() { + return from.getContentType(); + } + + @Override + public Headers getHeaders() { + return from.getHeaders(); + } + + @Override + public void writeToOutput(OutputStream out) throws IOException { + out.write(body); + } + + + @Override + public byte[] getBodyAsBytes() { + return body; + } + + /** + * Returns the buffered body of the event as a string + * + * @return the body of the event as a string + */ + @Override + public String getBodyAsString() { + return new String(body); + } + + } + + private FnTestingRule() { + } + + + public void addFeature(FnTestingRuleFeature f) { + this.features.add(f); + } + + public void addInput(InputEvent input) { + pendingInput.add(input); + } + + /** + * Create an instance of the testing {@link Rule}, with Flows support + * + * @return a new test rule + */ + public static FnTestingRule createDefault() { + return new FnTestingRule(); + } + + + /** + * Add a config variable to the function for the test + *

+ * Config names will be translated to upper case with hyphens and spaces translated to _. Clashing config keys will + * be overwritten. + * + * @param key the configuration key + * @param value the configuration value + * @return the current test rule + */ + public FnTestingRule setConfig(String key, String value) { + config.put(key.toUpperCase().replaceAll("[- ]", "_"), value); + return this; + } + + /** + * Add a class or package name to be forked during the test. + * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. + * + * @param prefix A class name or package prefix, such as "com.example.foo." + */ + public FnTestingRule addSharedClassPrefix(String prefix) { + sharedPrefixes.add(prefix); + return this; + } + + /** + * Add a class to be forked during the test. + * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. + * + * @param cls A class + */ + public FnTestingRule addSharedClass(Class cls) { + sharedPrefixes.add("=" + cls.getName()); + return this; + } + + @Override + public Statement apply(Statement base, Description description) { + return base; + } + + /** + * Create an HTTP event builder for the function + * + * @return a new event builder + */ + public FnEventBuilderJUnit4 givenEvent() { + return new DefaultFnEventBuilder(); + } + + /** + * Runs the function runtime with the specified class and method (and waits for Flow stages to finish + * if the test spawns any flows) + * + * @param cls class to thenRun + * @param method the method name + */ + public void thenRun(Class cls, String method) { + thenRun(cls.getName(), method); + } + + + public static class TestCodec implements EventCodec { + private final List input; + private final List output; + + public TestCodec(List input, List output) { + this.input = input; + this.output = output; + } + + @Override + public void runCodec(Handler h) { + for (InputEvent in : input) { + try { + output.add(new TestOutput(h.handle(in))); + } catch (IOException e) { + throw new RuntimeException("Unexpected exception in test", e); + } + } + } + } + + /** + * Runs the function runtime with the specified class and method (and waits for Flow stages to finish + * if the test spawns any Flow) + * + * @param cls class to thenRun + * @param method the method name + */ + public void thenRun(String cls, String method) { + final ClassLoader functionClassLoader; + Class c = null; + try { + // Trick to work around Maven class loader separation + // if passed class is a valid class then set the classloader to the same as the class's loader + c = Class.forName(cls); + } catch (Exception ignored) { + // TODO don't fall through here + } + if (c != null) { + functionClassLoader = c.getClassLoader(); + } else { + functionClassLoader = getClass().getClassLoader(); + } + + PrintStream oldSystemOut = System.out; + PrintStream oldSystemErr = System.err; + + for (FnTestingRuleFeature f : features) { + f.prepareTest(functionClassLoader, oldSystemErr, cls, method); + } + + Map mutableEnv = new HashMap<>(); + + try { + PrintStream functionErr = new PrintStream(new TeeOutputStream(stdErr, oldSystemErr)); + System.setOut(functionErr); + System.setErr(functionErr); + + mutableEnv.putAll(config); + mutableEnv.putAll(eventEnv); + mutableEnv.put("FN_FORMAT", "http-stream"); + mutableEnv.put("FN_FN_ID","myFnID"); + mutableEnv.put("FN_APP_ID","myAppID"); + + FnTestingClassLoader forked = new FnTestingClassLoader(functionClassLoader, sharedPrefixes); + if (forked.isShared(cls)) { + oldSystemErr.println("WARNING: The function class under test is shared with the test ClassLoader."); + oldSystemErr.println(" This may result in unexpected behaviour around function initialization and configuration."); + } + for (FnTestingRuleFeature f : features) { + f.prepareFunctionClassLoader(forked); + } + + TestCodec codec = new TestCodec(pendingInput, output); + + lastExitCode = forked.run( + mutableEnv, + codec, + functionErr, + cls + "::" + method); + + stdErr.flush(); + + for (FnTestingRuleFeature f : features) { + f.afterTestComplete(); + } + } catch (Exception e) { + throw new RuntimeException("internal error raised by entry point or flushing the test streams", e); + } finally { + System.out.flush(); + System.err.flush(); + System.setOut(oldSystemOut); + System.setErr(oldSystemErr); + + } + } + + /** + * Get the exit code from the most recent invocation + * 0 = success + * 1 = failed + * 2 = not run due to initialization error + */ + public int getLastExitCode() { + return lastExitCode; + } + + /** + * Get the StdErr stream returned by the function as a byte array + * + * @return the StdErr stream as bytes from the runtime + */ + public byte[] getStdErr() { + return stdErr.toByteArray(); + } + + /** + * Gets the StdErr stream returned by the function as a String + * + * @return the StdErr stream as a string from the function + */ + public String getStdErrAsString() { + return new String(stdErr.toByteArray()); + } + + /** + * Parses any pending HTTP responses on the functions output stream and returns the corresponding FnResult instances + * + * @return a list of Parsed HTTP responses (as {@link FnResult}s) from the function runtime output + */ + public List getResults() { + return output; + } + + /** + * Convenience method to get the one and only parsed http response expected on the output of the function + * + * @return a single parsed HTTP response from the function runtime output + * @throws IllegalStateException if zero or more than one responses were produced + */ + public FnResult getOnlyResult() { + List results = getResults(); + if (results.size() == 1) { + return results.get(0); + } + throw new IllegalStateException("One and only one response expected, but " + results.size() + " responses were generated."); + } + + + public List getSharedPrefixes() { + return Collections.unmodifiableList(sharedPrefixes); + } + + public Map getConfig() { + return Collections.unmodifiableMap(config); + } + + public Map getEventEnv() { + return Collections.unmodifiableMap(eventEnv); + } + + + /** + * Builds a mocked input event into the function runtime + */ + private class DefaultFnEventBuilder implements FnEventBuilderJUnit4 { + + FnHttpEventBuilder builder = new FnHttpEventBuilder(); + + + @Override + public FnEventBuilder withHeader(String key, String value) { + builder.withHeader(key, value); + return this; + } + + @Override + public FnEventBuilder withBody(InputStream body) throws IOException { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(byte[] body) { + builder.withBody(body); + return this; + } + + @Override + public FnEventBuilder withBody(String body) { + builder.withBody(body); + return this; + } + + + @Override + public FnTestingRule enqueue() { + + pendingInput.add(builder.buildEvent()); + return FnTestingRule.this; + } + + + @Override + public FnTestingRule enqueue(int n) { + if (n <= 0) { + throw new IllegalArgumentException("Invalid count"); + } + for (int i = 0; i < n; i++) { + enqueue(); + } + return FnTestingRule.this; + } + + + } + +} diff --git a/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRuleFeature.java b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRuleFeature.java new file mode 100644 index 00000000..adbf3c8f --- /dev/null +++ b/testing-junit4/src/main/java/com/fnproject/fn/testing/FnTestingRuleFeature.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +import java.io.PrintStream; + +/** + * Created on 07/09/2018. + *

+ * (c) 2018 Oracle Corporation + */ +public interface FnTestingRuleFeature { + + /** + * Prepares a test + * @param functionClassLoader + * @param stderr + * @param cls + * @param method + */ + void prepareTest(ClassLoader functionClassLoader, PrintStream stderr, String cls, String method); + + + void prepareFunctionClassLoader(FnTestingClassLoader cl); + + + void afterTestComplete(); +} diff --git a/testing-junit4/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java b/testing-junit4/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java new file mode 100644 index 00000000..e8d8be43 --- /dev/null +++ b/testing-junit4/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2019, 2020 Oracle and/or its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.fnproject.fn.testing; + +import com.fnproject.fn.api.Headers; +import com.fnproject.fn.api.InputEvent; +import com.fnproject.fn.api.OutputEvent; +import com.fnproject.fn.api.RuntimeContext; +import org.apache.commons.io.IOUtils; +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; + +public class FnTestingRuleTest { + + public static Map configuration; + public static InputEvent inEvent; + public static List capturedInputs = new ArrayList<>(); + public static List capturedBodies = new ArrayList<>(); + + @Rule + public FnTestingRule fn = FnTestingRule.createDefault(); + private final String exampleBaseUrl = "http://www.example.com"; + + @Before + public void reset() { + fn.addSharedClass(FnTestingRuleTest.class); + fn.addSharedClass(InputEvent.class); + + + FnTestingRuleTest.configuration = null; + FnTestingRuleTest.inEvent = null; + FnTestingRuleTest.capturedInputs = new ArrayList<>(); + FnTestingRuleTest.capturedBodies = new ArrayList<>(); + } + + + public static class TestFn { + private RuntimeContext ctx; + + public TestFn(RuntimeContext ctx) { + this.ctx = ctx; + } + + public void copyConfiguration() { + configuration = new HashMap<>(ctx.getConfiguration()); + } + + public void copyInputEvent(InputEvent inEvent) { + FnTestingRuleTest.inEvent = inEvent; + } + + public void err() { + throw new RuntimeException("ERR"); + } + + public void captureInput(InputEvent in) { + capturedInputs.add(in); + capturedBodies.add(in.consumeBody(TestFn::consumeToBytes)); + } + + private static byte[] consumeToBytes(InputStream is) { + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + IOUtils.copy(is, bos); + return bos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + public OutputEvent echoInput(InputEvent in) { + byte[] result = in.consumeBody(TestFn::consumeToBytes); + return OutputEvent.fromBytes(result, OutputEvent.Status.Success, "application/octet-stream"); + } + + } + + + @Test + public void shouldSetEnvironmentInsideFnScope() { + fn.givenEvent().enqueue(); + fn.setConfig("CONFIG_FOO", "BAR"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); + + Assertions.assertThat(configuration).containsEntry("CONFIG_FOO", "BAR"); + } + + + @Test + public void shouldCleanEnvironmentOfSpecialVarsInsideFnScope() { + fn.givenEvent().enqueue(); + fn.setConfig("CONFIG_FOO", "BAR"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); + + Assertions.assertThat(configuration).doesNotContainKeys("APP_NAME", "ROUTE", "METHOD", "REQUEST_URL"); + } + + + @Test + public void shouldHandleErrors() { + fn.givenEvent().enqueue(); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "err"); + + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.FunctionError); + Assertions.assertThat(fn.getStdErrAsString()).contains("An error occurred in function: ERR"); + } + + + @Test + public void configShouldNotOverrideIntrinsicHeaders() { + fn.givenEvent().enqueue(); + fn.setConfig("Fn-Call-Id", "BAR"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyInputEvent"); + + Assertions.assertThat(inEvent.getCallID()).isEqualTo("callId"); + } + + + @Test + public void configShouldBeCaptitalisedAndReplacedWithUnderscores() {// Basic test + // Test uppercasing and mangling of keys + fn.givenEvent().enqueue(); + + fn.setConfig("some-key-with-dashes", "some-value"); + + fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); + + Assertions.assertThat(configuration).containsEntry("SOME_KEY_WITH_DASHES", "some-value"); + + } + + + @Test + public void shouldSendEventDataToSDKInputEvent() { + + fn.setConfig("SOME_CONFIG", "SOME_VALUE"); + fn.givenEvent() + .withHeader("FOO", "BAR, BAZ") + .withHeader("FEH", "") + .withBody("Body") // body as string + .enqueue(); + + fn.thenRun(TestFn.class, "captureInput"); + + FnResult result = fn.getOnlyResult(); + Assertions.assertThat(result.getBodyAsString()).isEmpty(); + Assertions.assertThat(result.getStatus()).isEqualTo(OutputEvent.Status.Success); + + InputEvent event = capturedInputs.get(0); + Assertions.assertThat(event.getHeaders().asMap()) + .contains(headerEntry("FOO", "BAR, BAZ")) + .contains(headerEntry("FEH", "")); + Assertions.assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); + } + + + @Test + public void shouldEnqueueMultipleDistinctEvents() { + fn.setConfig("SOME_CONFIG", "SOME_VALUE"); + fn.givenEvent() + .withHeader("FOO", "BAR") + .withBody("Body") // body as string + .enqueue(); + + + fn.givenEvent() + .withHeader("FOO2", "BAR2") + .withBody("Body2") // body as string + .enqueue(); + + fn.thenRun(TestFn.class, "captureInput"); + + FnResult result = fn.getResults().get(0); + Assertions.assertThat(result.getBodyAsString()).isEmpty(); + Assertions.assertThat(result.getStatus()).isEqualTo(OutputEvent.Status.Success); + + InputEvent event = capturedInputs.get(0); + Assertions.assertThat(event.getHeaders().asMap()).contains(headerEntry("FOO", "BAR")); + Assertions.assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); + + + FnResult result2 = fn.getResults().get(1); + Assertions.assertThat(result2.getBodyAsString()).isEmpty(); + Assertions.assertThat(result2.getStatus()).isEqualTo(OutputEvent.Status.Success); + + InputEvent event2 = capturedInputs.get(1); + Assertions.assertThat(event2.getHeaders().asMap()).contains(headerEntry("FOO2", "BAR2")); + Assertions.assertThat(capturedBodies.get(1)).isEqualTo("Body2".getBytes()); + } + + + @Test + public void shouldEnqueueMultipleIdenticalEvents() { + fn.givenEvent() + .withHeader("FOO", "BAR") + .withHeader("Content-Type", "application/octet-stream") + .withBody("Body") // body as string + .enqueue(10); + + fn.thenRun(TestFn.class, "echoInput"); + + List results = fn.getResults(); + Assertions.assertThat(results).hasSize(10); + + + results.forEach((r) -> { + Assertions.assertThat(r.getStatus()).isEqualTo(OutputEvent.Status.Success); + + }); + } + + + @Test + public void shouldEnqueuIndependentEventsWithInputStreams() throws IOException { + fn.givenEvent() + .withBody(new ByteArrayInputStream("Body".getBytes())) // body as string + .enqueue(); + + fn.givenEvent() + .withBody(new ByteArrayInputStream("Body1".getBytes())) // body as string + .enqueue(); + + fn.thenRun(TestFn.class, "echoInput"); + + List results = fn.getResults(); + Assertions.assertThat(results).hasSize(2); + + Assertions.assertThat(results.get(0).getBodyAsString()).isEqualTo("Body"); + Assertions.assertThat(results.get(1).getBodyAsString()).isEqualTo("Body1"); + } + + @Test + public void shouldHandleBodyAsInputStream() throws IOException { + fn.givenEvent().withBody(new ByteArrayInputStream("FOO BAR".getBytes())).enqueue(); + + fn.thenRun(TestFn.class, "captureInput"); + + Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(OutputEvent.Status.Success); + Assertions.assertThat(capturedBodies.get(0)).isEqualTo("FOO BAR".getBytes()); + } + + // TODO move this to HTTP gateway +// @Test +// public void shouldLeaveQueryParamtersOffIfNotSpecified() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl); +// } +// +// @Test +// public void shouldPrependQuestionMarkForFirstQueryParam() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var", "val") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// Assertions.assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val"); +// } +// +// @Test +// public void shouldHandleMultipleQueryParameters() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var1", "val1") +// .withQueryParameter("var2", "val2") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var1=val1&var2=val2"); +// } +// +// @Test +// public void shouldHandleMultipleQueryParametersWithSameKey() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var", "val1") +// .withQueryParameter("var", "val2") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val1&var=val2"); +// } +// +// @Test +// public void shouldUrlEncodeQueryParameterKey() { +// fn.givenEvent() +// .withRequestUrl(exampleBaseUrl) +// .withQueryParameter("&", "val") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?%26=val"); +// } +// +// @Test +// public void shouldHandleQueryParametersWithSpaces() { +// fn.givenEvent() +// .withRequestUrl(exampleBaseUrl) +// .withQueryParameter("my var", "this val") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?my+var=this+val"); +// } +// +// @Test +// public void shouldUrlEncodeQueryParameterValue() { +// String baseUrl = "www.example.com"; +// fn.givenEvent() +// .withRequestUrl(baseUrl) +// .withQueryParameter("var", "&") +// .enqueue(); +// fn.thenRun(TestFn.class, "copyInputEvent"); +// +// Assertions.assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=%26"); +// } + + private static Map.Entry> headerEntry(String key, String... values) { + return new AbstractMap.SimpleEntry<>(Headers.canonicalKey(key), Arrays.asList(values)); + } +} diff --git a/testing/pom.xml b/testing/pom.xml index 567b31ff..afa719cc 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -1,4 +1,22 @@ + + @@ -8,30 +26,17 @@ 1.0.0-SNAPSHOT 4.0.0 - + testing testing - - UTF-8 - 1.2.2 - - com.fnproject.fn runtime - ${project.version} - - - junit - junit - ${junit.version} - org.assertj - assertj-core - ${assertj-core.version} - test + com.fnproject.fn + testing-core @@ -40,7 +45,6 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.0.0-M1 attach-javadocs @@ -50,11 +54,6 @@ - - org.pitest - pitest-maven - ${pitest.version} - diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java b/testing/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java deleted file mode 100644 index 9fe4a5b0..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/FnHttpEventBuilder.java +++ /dev/null @@ -1,178 +0,0 @@ -package com.fnproject.fn.testing; - -import org.apache.http.impl.io.ContentLengthInputStream; -import org.apache.http.impl.io.HttpTransportMetricsImpl; -import org.apache.http.impl.io.SessionInputBufferImpl; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.io.SequenceInputStream; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.util.*; -import java.util.stream.Collectors; - -class FnHttpEventBuilder { - private Map> queryParams = new TreeMap<>(); - private boolean streamRead = false; - private String method; - private String appName; - private String route; - private String requestUrl; - private byte[] bodyBytes = new byte[0]; - private InputStream bodyStream; - private int contentLength = 0; - private Map headers = new HashMap<>(); - - - public FnHttpEventBuilder withHeader(String key, String value) { - Objects.requireNonNull(key, "key"); - Objects.requireNonNull(value, "value"); - headers.put(key, value); - return this; - } - - public FnHttpEventBuilder withBody(InputStream body, int contentLength) { - Objects.requireNonNull(body, "body"); - if (contentLength < 0) { - throw new IllegalArgumentException("Invalid contentLength"); - } - // This is for safety. Because we concatenate events, an input stream shorter than content length will cause - // the implementation to continue reading through to the next http request. We need to avoid a sort of - // buffer overrun. - // FIXME: Make InputStream handling simpler. - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - sib.bind(body); - this.bodyStream = new ContentLengthInputStream(sib, contentLength); - this.contentLength = contentLength; - return this; - } - - public FnHttpEventBuilder withBody(byte[] body) { - Objects.requireNonNull(body, "body"); - this.bodyBytes = body; - this.contentLength = body.length; - this.bodyStream = null; - return this; - } - - public FnHttpEventBuilder withBody(String body) { - byte stringAsBytes[] = Objects.requireNonNull(body, "body").getBytes(); - return withBody(stringAsBytes); - } - - public FnHttpEventBuilder withRoute(String route) { - Objects.requireNonNull(route, "route"); - this.route = route; - return this; - } - - public FnHttpEventBuilder withMethod(String method) { - Objects.requireNonNull(method, "method"); - this.method = method.toUpperCase(); - return this; - } - - public FnHttpEventBuilder withAppName(String appName) { - Objects.requireNonNull(appName, "appName"); - this.appName = appName; - return this; - } - - public FnHttpEventBuilder withRequestUrl(String requestUrl) { - Objects.requireNonNull(requestUrl, "requestUrl"); - this.requestUrl = requestUrl; - return this; - } - - private String buildQueryParams() { - return queryParams.entrySet().stream() - .flatMap((e) -> e.getValue().stream() - .map((v) -> urlEncode(e.getKey()) + "=" + urlEncode(v))) - .collect(Collectors.joining("&")); - } - - - public FnHttpEventBuilder withQueryParameter(String key, String value) { - if (!this.queryParams.containsKey(key)) { - this.queryParams.put(key, new ArrayList<>()); - } - this.queryParams.get(key).add(value); - return this; - } - - public FnHttpEventBuilder withHeaders(Map headers) { - this.headers.putAll(headers); - return this; - } - - private String urlEncode(String str) { - try { - return URLEncoder.encode(str, "utf-8"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Your jvm doesn't support UTF-8, cannot continue."); - } - } - - private InputStream bodyAsStream() { - if (bodyStream != null) { - if (streamRead) { - throw new IllegalStateException("events with an overridden input stream can only be enqueued once"); - } - streamRead = true; - return bodyStream; - } else { - return new ByteArrayInputStream(bodyBytes); - } - } - - private void verify() { - Objects.requireNonNull(method, "method not set"); - Objects.requireNonNull(appName, "appName not set"); - Objects.requireNonNull(route, "route not set"); - Objects.requireNonNull(requestUrl, "requestUrl not set"); - - } - - public Map currentEventEnv() { - verify(); - Map env = new HashMap<>(); - headers.forEach((k, v) -> env.put("FN_HEADER_" + k.toUpperCase().replaceAll("-", "_"), v)); - env.put("FN_METHOD", method); - env.put("FN_APP_NAME", appName); - env.put("FN_PATH", route); - env.put("FN_REQUEST_URL", requestUrl); - return env; - } - - public InputStream currentEventInputStream() { - verify(); - - String queryParamsFullString = buildQueryParams(); - StringBuilder inputString = new StringBuilder(); - - inputString.append(method); - inputString.append(" / HTTP/1.1\r\n"); - inputString.append("Fn_App_name: ").append(appName).append("\r\n"); - inputString.append("Fn_Method: ").append(method).append("\r\n"); - inputString.append("Fn_Path: ").append(route).append("\r\n"); - inputString.append("Fn_Request_url: ").append(requestUrl); - if (!queryParamsFullString.isEmpty()) { - inputString.append("?").append(queryParamsFullString); - } - inputString.append("\r\n"); - - - inputString.append("Content-length: ").append(Integer.toString(contentLength)).append("\r\n"); - headers.forEach((k, v) -> inputString.append(k).append(": ").append(String.join(", ", v)).append("\r\n")); - - - inputString.append("\r\n"); - - return new SequenceInputStream( - new ByteArrayInputStream(inputString.toString().getBytes()), - bodyAsStream()); - } - - -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnResult.java b/testing/src/main/java/com/fnproject/fn/testing/FnResult.java deleted file mode 100644 index cbfadb6e..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/FnResult.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.api.Headers; - -/** - * A simple abstraction for a parsed HTTP response returned by a function - */ -public interface FnResult { - /** - * Returns the body of the function result as a byte array - * - * @return the function response body - */ - byte[] getBodyAsBytes(); - - /** - * Returns the body of the function response as a string - * - * @return a function response body - */ - String getBodyAsString(); - - /** - * A map of the headers returned by the function - *

- * These are squashed so duplicated headers will be ignored (takes the first header). - * - * @return a map of headers - */ - Headers getHeaders(); - - /** - * Returns the HTTP status code of the function response - * - * @return the HTTP status code returned by the function - */ - int getStatus(); - - /** - * Determine if the status code corresponds to a successful invocation - * - * @return true if the status code indicates success - */ - default boolean isSuccess() { - return 100 <= getStatus() && getStatus() < 400; - } -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FnTestingRule.java b/testing/src/main/java/com/fnproject/fn/testing/FnTestingRule.java deleted file mode 100644 index 77daf715..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/FnTestingRule.java +++ /dev/null @@ -1,640 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fnproject.fn.api.Headers; -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.QueryParameters; -import com.fnproject.fn.api.flow.Flow; -import com.fnproject.fn.api.flow.FlowCompletionException; -import com.fnproject.fn.api.flow.FunctionInvocationException; -import com.fnproject.fn.api.flow.HttpMethod; -import com.fnproject.fn.api.flow.PlatformException; -import com.fnproject.fn.runtime.flow.APIModel; -import com.fnproject.fn.runtime.flow.BlobResponse; -import com.fnproject.fn.runtime.flow.BlobStoreClient; -import com.fnproject.fn.runtime.flow.CodeLocation; -import com.fnproject.fn.runtime.flow.CompleterClient; -import com.fnproject.fn.runtime.flow.CompleterClientFactory; -import com.fnproject.fn.runtime.flow.CompletionId; -import com.fnproject.fn.runtime.flow.DefaultHttpResponse; -import com.fnproject.fn.runtime.flow.FlowContinuationInvoker; -import com.fnproject.fn.runtime.flow.FlowId; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.output.TeeOutputStream; -import org.apache.http.HttpResponse; -import org.apache.http.NoHttpResponseException; -import org.apache.http.impl.io.ContentLengthInputStream; -import org.apache.http.impl.io.DefaultHttpResponseParser; -import org.apache.http.impl.io.HttpTransportMetricsImpl; -import org.apache.http.impl.io.IdentityInputStream; -import org.apache.http.impl.io.SessionInputBufferImpl; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runners.model.Statement; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.PrintStream; -import java.io.SequenceInputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; - -import static com.fnproject.fn.runtime.flow.RemoteFlowApiClient.CONTENT_TYPE_HEADER; - -/** - * Testing {@link org.junit.Rule} for fn Java FDK functions. - *

- * This interface facilitates: - *

    - *
  • The creation of an in-memory environment replicating the functionality of the {@code fn} service
  • - *
  • The creation of input events passed to a user function using {@link #givenEvent()}
  • - *
  • The verification of function behaviour by accessing output represented by {@link FnResult} instances.
  • - *
- *

Example Usage:

- *
{@code
- * public class MyFunctionTest {
- *     {@literal @}Rule
- *     public final FnTestingRule testing = FnTestingRule.createDefault();
- *
- *     {@literal @}Test
- *     public void myTest() {
- *         // Create an event to invoke MyFunction and put it into the event queue
- *         fn.givenEvent()
- *            .withAppName("alpha")
- *            .withRoute("/bravo")
- *            .withRequestUrl("http://charlie/alpha/bravo")
- *            .withMethod("POST")
- *            .withHeader("FOO", "BAR")
- *            .withBody("Body")
- *            .enqueue();
- *
- *         // Run MyFunction#handleRequest using the built event queue from above
- *         fn.thenRun(MyFunction.class, "handleRequest");
- *
- *         // Get the function result and check it for correctness
- *         FnResult result = fn.getOnlyResult();
- *         assertThat(result.getStatus()).isEqualTo(200);
- *         assertThat(result.getBodyAsString()).isEqualTo("expected return value of my function");
- *     }
- * }}
- */ -public final class FnTestingRule implements TestRule { - private final Map config = new HashMap<>(); - private Map eventEnv = new HashMap<>(); - private boolean hasEvents = false; - private InputStream pendingInput = new ByteArrayInputStream(new byte[0]); - private ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); - private ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); - private Map functionStubs = new HashMap<>(); - public static InMemCompleter completer = null; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private final List sharedPrefixes = new ArrayList<>(); - private int lastExitCode; - - { - // Internal shared classes required to bridge completer into tests - addSharedClassPrefix("java."); - addSharedClassPrefix("javax."); - addSharedClassPrefix("sun."); - addSharedClassPrefix("jdk."); - - addSharedClass(CompleterClient.class); - addSharedClass(BlobStoreClient.class); - addSharedClass(BlobResponse.class); - - addSharedClass(CompleterClientFactory.class); - addSharedClass(CompletionId.class); - addSharedClass(FlowId.class); - addSharedClass(Flow.FlowState.class); - addSharedClass(CodeLocation.class); - addSharedClass(Headers.class); - addSharedClass(HttpMethod.class); - addSharedClass(com.fnproject.fn.api.flow.HttpRequest.class); - addSharedClass(com.fnproject.fn.api.flow.HttpResponse.class); - addSharedClass(QueryParameters.class); - addSharedClass(InputEvent.class); - addSharedClass(OutputEvent.class); - addSharedClass(FlowCompletionException.class); - addSharedClass(FunctionInvocationException.class); - addSharedClass(PlatformException.class); - - } - - private FnTestingRule() { - } - - /** - * Create an instance of the testing {@link org.junit.Rule}, with Flows support - * - * @return a new test rule - */ - public static FnTestingRule createDefault() { - return new FnTestingRule(); - } - - - /** - * Add a config variable to the function for the test - *

- * Config names will be translated to upper case with hyphens and spaces translated to _. Clashing config keys will - * be overwritten. - * - * @param key the configuration key - * @param value the configuration value - * @return the current test rule - */ - public FnTestingRule setConfig(String key, String value) { - config.put(key.toUpperCase().replaceAll("[- ]", "_"), value); - return this; - } - - /** - * Add a class or package name to be forked during the test. - * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. - * - * @param prefix A class name or package prefix, such as "com.example.foo." - */ - public FnTestingRule addSharedClassPrefix(String prefix) { - sharedPrefixes.add(prefix); - return this; - } - - /** - * Add a class to be forked during the test. - * The test will be run under the aegis of a classloader that duplicates the class hierarchy named. - * - * @param cls A class - */ - public FnTestingRule addSharedClass(Class cls) { - sharedPrefixes.add("=" + cls.getName()); - return this; - } - - @Override - public Statement apply(Statement base, Description description) { - return base; - } - - /** - * Create an HTTP event builder for the function - * - * @return a new event builder - */ - public FnEventBuilder givenEvent() { - return new DefaultFnEventBuilder(); - } - - /** - * Runs the function runtime with the specified class and method (and waits for Flow stages to finish - * if the test spawns any flows) - * - * @param cls class to thenRun - * @param method the method name - */ - public void thenRun(Class cls, String method) { - thenRun(cls.getName(), method); - } - - - /** - * Runs the function runtime with the specified class and method (and waits for Flow stages to finish - * if the test spawns any Flow) - * - * @param cls class to thenRun - * @param method the method name - */ - public void thenRun(String cls, String method) { - final ClassLoader functionClassLoader; - Class c = null; - try { - // Trick to work around Maven class loader separation - // if passed class is a valid class then set the classloader to the same as the class's loader - c = Class.forName(cls); - } catch (Exception ignored) { - // TODO don't fall through here - } - if (c != null) { - functionClassLoader = c.getClassLoader(); - } else { - functionClassLoader = getClass().getClassLoader(); - } - - PrintStream oldSystemOut = System.out; - PrintStream oldSystemErr = System.err; - - InMemCompleter.CompleterInvokeClient client = new TestRuleCompleterInvokeClient(functionClassLoader, oldSystemErr, cls, method); - - InMemCompleter.FnInvokeClient fnInvokeClient = new TestRuleFnInvokeClient(); - - // FlowContinuationInvoker.setTestingMode(true); - // The following must be a static: otherwise the factory (the lambda) will not be serializable. - completer = new InMemCompleter(client, fnInvokeClient); - - //TestSupport.installCompleterClientFactory(completer, oldSystemErr); - - - Map mutableEnv = new HashMap<>(); - - try { - PrintStream functionOut = new PrintStream(stdOut); - PrintStream functionErr = new PrintStream(new TeeOutputStream(stdErr, oldSystemErr)); - System.setOut(functionErr); - System.setErr(functionErr); - - mutableEnv.putAll(config); - mutableEnv.putAll(eventEnv); - mutableEnv.put("FN_FORMAT", "http"); - - FnTestingClassLoader forked = new FnTestingClassLoader(functionClassLoader, sharedPrefixes); - if (forked.isShared(cls)) { - oldSystemErr.println("WARNING: The function class under test is shared with the test ClassLoader."); - oldSystemErr.println(" This may result in unexpected behaviour around function initialization and configuration."); - } - forked.setCompleterClient(completer); - lastExitCode = forked.run( - mutableEnv, - pendingInput, - functionOut, - functionErr, - cls + "::" + method); - - stdOut.flush(); - stdErr.flush(); - - completer.awaitTermination(); - } catch (Exception e) { - throw new RuntimeException("internal error raised by entry point or flushing the test streams", e); - } finally { - System.out.flush(); - System.err.flush(); - System.setOut(oldSystemOut); - System.setErr(oldSystemErr); - - } - } - - /** - * Get the exit code from the most recent invocation - * 0 = success - * 1 = failed - * 2 = not run due to initialization error - */ - public int getLastExitCode() { - return lastExitCode; - } - - /** - * Get the StdErr stream returned by the function as a byte array - * - * @return the StdErr stream as bytes from the runtime - */ - public byte[] getStdErr() { - return stdErr.toByteArray(); - } - - /** - * Gets the StdErr stream returned by the function as a String - * - * @return the StdErr stream as a string from the function - */ - public String getStdErrAsString() { - return new String(stdErr.toByteArray()); - } - - /** - * Parses any pending HTTP responses on the functions output stream and returns the corresponding FnResult instances - * - * @return a list of Parsed HTTP responses (as {@link FnResult}s) from the function runtime output - */ - public List getResults() { - return parseHttpStreamForResults(stdOut.toByteArray()); - } - - /** - * Convenience method to get the one and only parsed http response expected on the output of the function - * - * @return a single parsed HTTP response from the function runtime output - * @throws IllegalStateException if zero or more than one responses were produced - */ - public FnResult getOnlyResult() { - List results = getResults(); - if (results.size() == 1) { - return results.get(0); - } - throw new IllegalStateException("One and only one response expected, but " + results.size() + " responses were generated."); - } - - - private List parseHttpStreamForResults(byte[] httpStream) { - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - ByteArrayInputStream parseStream = new ByteArrayInputStream(httpStream); - sib.bind(parseStream); - - DefaultHttpResponseParser parser = new DefaultHttpResponseParser(sib); - List responses = new ArrayList<>(); - - while (true) { - try { - HttpResponse response = parser.parse(); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ContentLengthInputStream cis = new ContentLengthInputStream(sib, Long.parseLong(response.getFirstHeader("Content-length").getValue())); - - IOUtils.copy(cis, bos); - cis.close(); - byte[] body = bos.toByteArray(); - FnResult r = new FnResult() { - @Override - public byte[] getBodyAsBytes() { - return body; - } - - @Override - public String getBodyAsString() { - return new String(body); - } - - @Override - public Headers getHeaders() { - Map headers = new HashMap<>(); - Arrays.stream(response.getAllHeaders()).forEach((h) -> - headers.put(h.getName(), h.getValue())); - return Headers.fromMap(headers); - } - - @Override - public int getStatus() { - return response.getStatusLine().getStatusCode(); - } - }; - responses.add(r); - } catch (NoHttpResponseException e) { - break; - } catch (Exception e) { - throw new RuntimeException("Invalid HTTP response", e); - } - } - return responses; - } - - - public FnFunctionStubBuilder givenFn(String id) { - return new FnFunctionStubBuilder() { - @Override - public FnTestingRule withResult(byte[] result) { - return withAction((body) -> result); - } - - @Override - public FnTestingRule withFunctionError() { - return withAction((body) -> { - throw new FunctionError("simulated by testing platform"); - }); - } - - @Override - public FnTestingRule withPlatformError() { - return withAction((body) -> { - throw new PlatformError("simulated by testing platform"); - }); - } - - @Override - public FnTestingRule withAction(ExternalFunctionAction f) { - functionStubs.put(id, (HttpMethod method, Headers headers, byte[] body) -> { - try { - return new DefaultHttpResponse(200, Headers.emptyHeaders(), f.apply(body)); - } catch (FunctionError functionError) { - return new DefaultHttpResponse(500, Headers.emptyHeaders(), functionError.getMessage().getBytes()); - } catch (PlatformError platformError) { - throw new RuntimeException("Platform Error"); - } - }); - return FnTestingRule.this; - } - }; - } - - private interface FnFunctionStub { - com.fnproject.fn.api.flow.HttpResponse stubFunction(HttpMethod method, Headers headers, byte[] body); - } - - /** - * Builds a mocked input event into the function runtime - */ - private class DefaultFnEventBuilder implements FnEventBuilder { - - FnHttpEventBuilder builder = new FnHttpEventBuilder().withMethod("GET") - .withAppName("appName") - .withRoute("/route") - .withRequestUrl("http://example.com/r/appName/route"); - - - @Override - public FnEventBuilder withHeader(String key, String value) { - builder.withHeader(key, value); - return this; - } - - @Override - public FnEventBuilder withBody(InputStream body, int contentLength) { - builder.withBody(body, contentLength); - return this; - } - - @Override - public FnEventBuilder withBody(byte[] body) { - builder.withBody(body); - return this; - } - - @Override - public FnEventBuilder withBody(String body) { - builder.withBody(body); - return this; - } - - @Override - public FnEventBuilder withAppName(String appName) { - builder.withAppName(appName); - return this; - } - - @Override - public FnEventBuilder withRoute(String route) { - builder.withRoute(route); - return this; - } - - @Override - public FnEventBuilder withMethod(String method) { - builder.withMethod(method); - return this; - } - - @Override - public FnEventBuilder withRequestUrl(String requestUrl) { - builder.withRequestUrl(requestUrl); - return this; - - } - - @Override - public FnEventBuilder withQueryParameter(String key, String value) { - builder.withQueryParameter(key, value); - return this; - } - - @Override - public FnTestingRule enqueue() { - - // Only set env for first event. - if (!hasEvents) { - eventEnv.putAll(builder.currentEventEnv()); - } - hasEvents = true; - - pendingInput = new SequenceInputStream(pendingInput, builder.currentEventInputStream()); - return FnTestingRule.this; - } - - - @Override - public FnTestingRule enqueue(int n) { - if (n <= 0) { - throw new IllegalArgumentException("Invalid count"); - } - for (int i = 0; i < n; i++) { - enqueue(); - } - return FnTestingRule.this; - } - - - } - - private class TestRuleCompleterInvokeClient implements InMemCompleter.CompleterInvokeClient { - private final ClassLoader functionClassLoader; - private final PrintStream oldSystemErr; - private final String cls; - private final String method; - private final Set pool = new HashSet<>(); - - - public TestRuleCompleterInvokeClient(ClassLoader functionClassLoader, PrintStream oldSystemErr, String cls, String method) { - this.functionClassLoader = functionClassLoader; - this.oldSystemErr = oldSystemErr; - this.cls = cls; - this.method = method; - } - - - @Override - public APIModel.CompletionResult invokeStage(String fnId, FlowId flowId, CompletionId stageId, APIModel.Blob closure, List input) { - // Construct a new ClassLoader hierarchy with a copy of the statics embedded in the runtime. - // Initialise it appropriately. - FnTestingClassLoader fcl = new FnTestingClassLoader(functionClassLoader, sharedPrefixes); - fcl.setCompleterClient(completer); - - - APIModel.InvokeStageRequest request = new APIModel.InvokeStageRequest(); - request.stageId = stageId.getId(); - request.flowId = flowId.getId(); - request.closure = closure; - request.args = input; - - String inputBody = null; - try { - inputBody = objectMapper.writeValueAsString(request); - } catch (JsonProcessingException e) { - throw new IllegalStateException("Invalid request"); - } - - // oldSystemErr.println("Body\n" + new String(inputBody)); - - InputStream is = new FnHttpEventBuilder() - .withBody(inputBody) - .withAppName("appName") - .withRoute("/route").withRequestUrl("http://some/url") - .withMethod("POST") - .withHeader(CONTENT_TYPE_HEADER, "application/json") - .withHeader(FlowContinuationInvoker.FLOW_ID_HEADER, flowId.getId()).currentEventInputStream(); - - ByteArrayOutputStream output = new ByteArrayOutputStream(); - Map mutableEnv = new HashMap<>(); - PrintStream functionOut = new PrintStream(output); - PrintStream functionErr = new PrintStream(oldSystemErr); - - // Do we want to capture IO from continuations on the main log stream? - // System.setOut(functionErr); - // System.setErr(functionErr); - - mutableEnv.putAll(config); - mutableEnv.putAll(eventEnv); - mutableEnv.put("FN_FORMAT", "http"); - - - fcl.run( - mutableEnv, - is, - functionOut, - functionErr, - cls + "::" + method); - - - SessionInputBufferImpl sib = new SessionInputBufferImpl(new HttpTransportMetricsImpl(), 65535); - ByteArrayInputStream parseStream = new ByteArrayInputStream(output.toByteArray()); - sib.bind(parseStream); - DefaultHttpResponseParser parser = new DefaultHttpResponseParser(sib); - APIModel.CompletionResult r; - try { - // Read wrapping result, and throw it away - parser.parse(); - IdentityInputStream iis = new IdentityInputStream(sib); - byte[] responseBody = IOUtils.toByteArray(iis); - - APIModel.InvokeStageResponse response = objectMapper.readValue(responseBody, APIModel.InvokeStageResponse.class); - r = response.result; - - } catch (Exception e) { - oldSystemErr.println("Err\n" + e); - e.printStackTrace(oldSystemErr); - r = APIModel.CompletionResult.failure(APIModel.ErrorDatum.newError(APIModel.ErrorType.UnknownError, "Error reading fn Response:" + e.getMessage())); - } - - if (!r.successful) { - throw new ResultException(r.result); - } - return r; - - } - } - - private class TestRuleFnInvokeClient implements InMemCompleter.FnInvokeClient { - @Override - public CompletableFuture invokeFunction(String fnId, HttpMethod method, Headers headers, byte[] data) { - FnFunctionStub stub = functionStubs - .computeIfAbsent(fnId, (k) -> { - throw new IllegalStateException("Function was invoked that had no definition: " + k); - }); - - try { - return CompletableFuture.completedFuture(stub.stubFunction(method, headers, data)); - } catch (Exception e) { - CompletableFuture respFuture = new CompletableFuture<>(); - respFuture.completeExceptionally(e); - return respFuture; - } - } - } -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/FunctionError.java b/testing/src/main/java/com/fnproject/fn/testing/FunctionError.java deleted file mode 100644 index 840a969a..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/FunctionError.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.fnproject.fn.testing; - -/** - * An Exception that can be used in invocations of stubbed external functions to signal the failure of the external - * function due to a simulated error case of the function itself - */ -public class FunctionError extends Exception { - public FunctionError() { - } - - public FunctionError(String s) { - super(s); - } -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/HeaderWriter.java b/testing/src/main/java/com/fnproject/fn/testing/HeaderWriter.java deleted file mode 100644 index d8f9f227..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/HeaderWriter.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.fnproject.fn.testing; - -import java.io.IOException; -import java.io.OutputStream; - -class HeaderWriter { - final OutputStream os; - - HeaderWriter(OutputStream os) { - this.os = os; - } - - void writeHeader(String key, String value) throws IOException { - os.write((key + ": " + value + "\r\n").getBytes("ISO-8859-1")); - } -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/PlatformError.java b/testing/src/main/java/com/fnproject/fn/testing/PlatformError.java deleted file mode 100644 index 559d0e2c..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/PlatformError.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.fnproject.fn.testing; - -/** - * An Exception that can be used in invocations of stubbed external functions to signal the failure of the external - * function due to a simulated infrastructure error in the Oracle Functions platform - */ -public class PlatformError extends Exception { - public PlatformError() { - } - - public PlatformError(String s) { - super(s); - } -} diff --git a/testing/src/main/java/com/fnproject/fn/testing/ResultException.java b/testing/src/main/java/com/fnproject/fn/testing/ResultException.java deleted file mode 100644 index e2082b3c..00000000 --- a/testing/src/main/java/com/fnproject/fn/testing/ResultException.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.runtime.flow.APIModel; - -class ResultException extends RuntimeException { - private final APIModel.Datum datum; - - ResultException(APIModel.Datum datum) { - this.datum = datum; - } - - APIModel.CompletionResult toResult() { - return APIModel.CompletionResult.failure(datum); - } -} diff --git a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java b/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java deleted file mode 100644 index d46b8689..00000000 --- a/testing/src/test/java/com/fnproject/fn/testing/FnTestingRuleTest.java +++ /dev/null @@ -1,401 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.api.InputEvent; -import com.fnproject.fn.api.OutputEvent; -import com.fnproject.fn.api.RuntimeContext; -import org.apache.commons.io.IOUtils; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.*; - -import static org.assertj.core.api.Assertions.assertThat; - -public class FnTestingRuleTest { - - public static Map configuration; - public static InputEvent inEvent; - public static List capturedInputs = new ArrayList<>(); - public static List capturedBodies = new ArrayList<>(); - - @Rule - public FnTestingRule fn = FnTestingRule.createDefault(); - private final String exampleBaseUrl = "http://www.example.com"; - - @Before - public void reset() { - fn.addSharedClass(FnTestingRuleTest.class); - fn.addSharedClass(InputEvent.class); - - - FnTestingRuleTest.configuration = null; - FnTestingRuleTest.inEvent = null; - FnTestingRuleTest.capturedInputs = new ArrayList<>(); - FnTestingRuleTest.capturedBodies = new ArrayList<>(); - } - - - public static class TestFn { - private RuntimeContext ctx; - - public TestFn(RuntimeContext ctx) { - this.ctx = ctx; - } - - public void copyConfiguration() { - configuration = new HashMap<>(ctx.getConfiguration()); - } - - public void copyInputEvent(InputEvent inEvent) { - FnTestingRuleTest.inEvent = inEvent; - } - - public void err() { - throw new RuntimeException("ERR"); - } - - public void captureInput(InputEvent in) { - capturedInputs.add(in); - capturedBodies.add(in.consumeBody(TestFn::consumeToBytes)); - } - - private static byte[] consumeToBytes(InputStream is) { - try { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - IOUtils.copy(is, bos); - return bos.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - - public OutputEvent echoInput(InputEvent in) { - byte[] result = in.consumeBody(TestFn::consumeToBytes); - return OutputEvent.fromBytes(result, OutputEvent.SUCCESS, "application/octet-stream"); - } - - } - - - @Test - public void shouldSetEnvironmentInsideFnScope() { - fn.givenEvent().enqueue(); - fn.setConfig("CONFIG_FOO", "BAR"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); - - assertThat(configuration).containsEntry("CONFIG_FOO", "BAR"); - } - - - @Test - public void shouldCleanEnvironmentOfSpecialVarsInsideFnScope() { - fn.givenEvent().enqueue(); - fn.setConfig("CONFIG_FOO", "BAR"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); - - assertThat(configuration).doesNotContainKeys("APP_NAME", "ROUTE", "METHOD", "REQUEST_URL"); - } - - - @Test - public void shouldHandleErrors() { - fn.givenEvent().enqueue(); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "err"); - - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(500); - assertThat(fn.getStdErrAsString()).contains("An error occurred in function: ERR"); - } - - - @Test - public void configShouldNotOverrideIntrinsicHeaders() { - fn.givenEvent().enqueue(); - fn.setConfig("APP_NAME", "BAR"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getAppName()).isEqualTo("appName"); - } - - - @Test - public void configShouldBeCaptitalisedAndReplacedWithUnderscores() {// Basic test - // Test uppercasing and mangling of keys - fn.givenEvent().enqueue(); - - fn.setConfig("some-key-with-dashes", "some-value"); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyConfiguration"); - - assertThat(configuration).containsEntry("SOME_KEY_WITH_DASHES", "some-value"); - - } - - @Test - public void shouldSetArgsInFirstEvent() { - fn.givenEvent().withAppName("TEST_APP") - .withHeader("H1", "H2") - .withMethod("PUT") - .withRequestUrl("http://example.com/mytest") - .withRoute("/myroute") - .enqueue(); - - fn.thenRun(FnTestingRuleTest.TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getAppName()).isEqualTo("TEST_APP"); - assertThat(inEvent.getRoute()).isEqualTo("/myroute"); - assertThat(inEvent.getMethod()).isEqualTo("PUT"); - assertThat(inEvent.getRequestUrl()).isEqualTo("http://example.com/mytest"); - } - - - @Test - public void shouldSendEventDataToSDKInputEvent() { - final String APP_NAME = "alpha"; - final String ROUTE = "/bravo"; - final String REQUEST_URL = "http://charlie/alpha/bravo"; - final String METHOD = "POST"; - - fn.setConfig("SOME_CONFIG", "SOME_VALUE"); - fn.givenEvent() - .withAppName(APP_NAME) - .withRoute(ROUTE) - .withRequestUrl(REQUEST_URL) - .withMethod(METHOD) - .withHeader("FOO", "BAR, BAZ") - .withHeader("FEH", "") - .withBody("Body") // body as string - .enqueue(); - - fn.thenRun(TestFn.class, "captureInput"); - - FnResult result = fn.getOnlyResult(); - assertThat(result.getBodyAsString()).isEmpty(); - assertThat(result.getHeaders().getAll()).contains(headerEntry("Content-length", "0")); - assertThat(result.getStatus()).isEqualTo(200); - - InputEvent event = capturedInputs.get(0); - assertThat(event.getAppName()).isEqualTo(APP_NAME); - assertThat(event.getHeaders().getAll()) - .contains(headerEntry("FOO", "BAR, BAZ")) - .contains(headerEntry("FEH", "")); - assertThat(event.getMethod()).isEqualTo(METHOD); - assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); - } - - - @Test - public void shouldEnqueueMultipleDistinctEvents() { - fn.setConfig("SOME_CONFIG", "SOME_VALUE"); - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo") - .withRequestUrl("http://charlie/alpha/bravo") - .withMethod("POST") - .withHeader("FOO", "BAR") - .withBody("Body") // body as string - .enqueue(); - - - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo2") - .withRequestUrl("http://charlie/alpha/bravo2") - .withMethod("PUT") - .withHeader("FOO2", "BAR2") - .withBody("Body2") // body as string - .enqueue(); - - fn.thenRun(TestFn.class, "captureInput"); - - FnResult result = fn.getResults().get(0); - assertThat(result.getBodyAsString()).isEmpty(); - assertThat(result.getHeaders().getAll()).contains(headerEntry("Content-length", "0")); - assertThat(result.getStatus()).isEqualTo(200); - - InputEvent event = capturedInputs.get(0); - assertThat(event.getAppName()).isEqualTo("alpha"); - assertThat(event.getHeaders().getAll()).contains(headerEntry("FOO", "BAR")); - assertThat(event.getMethod()).isEqualTo("POST"); - assertThat(capturedBodies.get(0)).isEqualTo("Body".getBytes()); - - - FnResult result2 = fn.getResults().get(1); - assertThat(result2.getBodyAsString()).isEmpty(); - assertThat(result2.getHeaders().getAll()).contains(headerEntry("Content-length", "0")); - assertThat(result2.getStatus()).isEqualTo(200); - - InputEvent event2 = capturedInputs.get(1); - assertThat(event2.getAppName()).isEqualTo("alpha"); - assertThat(event2.getHeaders().getAll()).contains(headerEntry("FOO2", "BAR2")); - assertThat(event2.getMethod()).isEqualTo("PUT"); - assertThat(capturedBodies.get(1)).isEqualTo("Body2".getBytes()); - } - - - @Test - public void shouldEnqueueMultipleIdenticalEvents() { - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo") - .withRequestUrl("http://charlie/alpha/bravo") - .withMethod("POST") - .withHeader("FOO", "BAR") - .withBody("Body") // body as string - .enqueue(10); - - fn.thenRun(TestFn.class, "echoInput"); - - List results = fn.getResults(); - assertThat(results).hasSize(10); - - - results.forEach((r) -> { - assertThat(r.getStatus()).isEqualTo(200); - assertThat(r.getHeaders().getAll()) - .contains(headerEntry("Content-Type", "application/octet-stream")) - .contains(headerEntry("Content-length", String.valueOf("Body".getBytes().length))); - - }); - } - - - @Test(expected = IllegalStateException.class) - public void shouldNotAllowSecondEnqueueOnInputStreamInput() { - fn.givenEvent() - .withAppName("alpha") - .withRoute("/bravo") - .withRequestUrl("http://charlie/alpha/bravo") - .withMethod("POST") - .withHeader("FOO", "BAR") - .withBody(new ByteArrayInputStream("Body".getBytes()), 4) // body as string - .enqueue(2); - - - } - - @Test - public void shouldEnqueuIndependentEventsWithInputStreams() { - fn.givenEvent() - .withBody(new ByteArrayInputStream("Body".getBytes()), 4) // body as string - .enqueue(); - - fn.givenEvent() - .withBody(new ByteArrayInputStream("Body1".getBytes()), 5) // body as string - .enqueue(); - - fn.thenRun(TestFn.class, "echoInput"); - - List results = fn.getResults(); - assertThat(results).hasSize(2); - - assertThat(results.get(0).getBodyAsString()).isEqualTo("Body"); - assertThat(results.get(1).getBodyAsString()).isEqualTo("Body1"); - } - - @Test - public void shouldHandleBodyAsInputStream() { - fn.givenEvent().withBody(new ByteArrayInputStream("FOO BAR".getBytes()), 3).enqueue(); - - fn.thenRun(TestFn.class, "captureInput"); - - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); - assertThat(capturedBodies.get(0)).isEqualTo("FOO".getBytes()); - } - - @Test - public void shouldLeaveQueryParamtersOffIfNotSpecified() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl); - } - - @Test - public void shouldPrependQuestionMarkForFirstQueryParam() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var", "val") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - assertThat(fn.getOnlyResult().getStatus()).isEqualTo(200); - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val"); - } - - @Test - public void shouldHandleMultipleQueryParameters() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var1", "val1") - .withQueryParameter("var2", "val2") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var1=val1&var2=val2"); - } - - @Test - public void shouldHandleMultipleQueryParametersWithSameKey() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var", "val1") - .withQueryParameter("var", "val2") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=val1&var=val2"); - } - - @Test - public void shouldUrlEncodeQueryParameterKey() { - fn.givenEvent() - .withRequestUrl(exampleBaseUrl) - .withQueryParameter("&", "val") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?%26=val"); - } - - @Test - public void shouldHandleQueryParametersWithSpaces() { - fn.givenEvent() - .withRequestUrl(exampleBaseUrl) - .withQueryParameter("my var", "this val") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(exampleBaseUrl + "?my+var=this+val"); - } - - @Test - public void shouldUrlEncodeQueryParameterValue() { - String baseUrl = "www.example.com"; - fn.givenEvent() - .withRequestUrl(baseUrl) - .withQueryParameter("var", "&") - .enqueue(); - fn.thenRun(TestFn.class, "copyInputEvent"); - - assertThat(inEvent.getRequestUrl()).isEqualTo(baseUrl + "?var=%26"); - } - - private static Map.Entry headerEntry(String key, String value) { - return new AbstractMap.SimpleEntry<>(key, value); - } -} diff --git a/testing/src/test/java/com/fnproject/fn/testing/IntegrationTest.java b/testing/src/test/java/com/fnproject/fn/testing/IntegrationTest.java deleted file mode 100644 index 4de9171d..00000000 --- a/testing/src/test/java/com/fnproject/fn/testing/IntegrationTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.fnproject.fn.testing; - -import org.junit.Rule; -import org.junit.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class IntegrationTest { - - @Rule - public FnTestingRule fn = FnTestingRule.createDefault(); - - @Test - public void runIntegrationTests() { - - fn.givenFn("nonexistent/nonexistent") - .withFunctionError() - - .givenFn("appName/route") - .withAction((body) -> { - if (new String(body).equals("PASS")) { - return "okay".getBytes(); - } else { - throw new FunctionError("failed as demanded"); - } - }) - .givenEvent() - .withBody("") // or "1,5,6,32" to select a set of tests individually - .enqueue() - - .thenRun(ExerciseEverything.class, "handleRequest"); - - assertThat(fn.getResults().get(0).getBodyAsString()) - .endsWith("Everything worked\n"); - } -} diff --git a/testing/src/test/java/com/fnproject/fn/testing/WhenCompleteTest.java b/testing/src/test/java/com/fnproject/fn/testing/WhenCompleteTest.java deleted file mode 100644 index 7e44c78c..00000000 --- a/testing/src/test/java/com/fnproject/fn/testing/WhenCompleteTest.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.fnproject.fn.testing; - -import com.fnproject.fn.api.flow.Flows; -import org.junit.Rule; -import org.junit.Test; - -import java.util.concurrent.atomic.AtomicInteger; - -import static org.assertj.core.api.Assertions.assertThat; - -public class WhenCompleteTest { - @Rule - public FnTestingRule fn = FnTestingRule.createDefault(); - - public static AtomicInteger cas = new AtomicInteger(0); - - public static class TestFn { - public void handleRequest() { - Flows.currentFlow().completedValue(1) - .whenComplete((v, e) -> WhenCompleteTest.cas.compareAndSet(0, 1)) - .thenRun(() -> WhenCompleteTest.cas.compareAndSet(1, 2)); - } - } - - @Test - public void OverlappingFlowInvocationsShouldWork() { - fn.addSharedClass(WhenCompleteTest.class); - - cas.set(0); - - fn.givenEvent().enqueue(); - - fn.thenRun(TestFn.class, "handleRequest"); - - assertThat(cas.get()).isEqualTo(2); - } - -}