├── .github ├── .release-please-manifest.json ├── blunderbuss.yml ├── release-please-config.json ├── release-please.yml ├── release-trigger.yml ├── renovate.json └── workflows │ ├── buildpack-integration-test.yml │ ├── codeql.yml │ ├── conformance.yaml │ ├── lint.yaml │ ├── scorecard.yml │ └── unit.yaml ├── .gitignore ├── .kokoro ├── release.cfg └── release.sh ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── function-maven-plugin ├── CHANGELOG.md ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── google │ │ └── cloud │ │ └── functions │ │ └── plugin │ │ ├── DeployFunction.java │ │ └── RunFunction.java │ └── test │ └── java │ └── com │ └── google │ └── cloud │ └── functions │ └── plugin │ └── DeployFunctionTest.java ├── functions-framework-api ├── CHANGELOG.md ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── google │ └── cloud │ └── functions │ ├── BackgroundFunction.java │ ├── CloudEventsFunction.java │ ├── Context.java │ ├── HttpFunction.java │ ├── HttpMessage.java │ ├── HttpRequest.java │ ├── HttpResponse.java │ ├── RawBackgroundFunction.java │ └── TypedFunction.java ├── invoker ├── CHANGELOG.md ├── conformance │ ├── buildpack_pom.xml │ ├── pom.xml │ ├── prerun.sh │ └── src │ │ └── main │ │ └── java │ │ └── com │ │ └── google │ │ └── cloud │ │ └── functions │ │ └── conformance │ │ ├── BackgroundEventConformanceFunction.java │ │ ├── CloudEventsConformanceFunction.java │ │ ├── ConcurrentHttpConformanceFunction.java │ │ ├── HttpConformanceFunction.java │ │ └── TypedConformanceFunction.java ├── core │ ├── pom.xml │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── google │ │ │ └── cloud │ │ │ └── functions │ │ │ └── invoker │ │ │ ├── BackgroundFunctionExecutor.java │ │ │ ├── CloudEvents.java │ │ │ ├── CloudFunctionsContext.java │ │ │ ├── Event.java │ │ │ ├── GcfEvents.java │ │ │ ├── HttpFunctionExecutor.java │ │ │ ├── TypedFunctionExecutor.java │ │ │ ├── gcf │ │ │ ├── ExecutionIdUtil.java │ │ │ └── JsonLogHandler.java │ │ │ ├── http │ │ │ ├── HttpRequestImpl.java │ │ │ ├── HttpResponseImpl.java │ │ │ └── TimeoutFilter.java │ │ │ └── runner │ │ │ └── Invoker.java │ │ └── test │ │ ├── java │ │ ├── PackagelessHelloWorld.java │ │ └── com │ │ │ └── google │ │ │ └── cloud │ │ │ └── functions │ │ │ └── invoker │ │ │ ├── BackgroundFunctionExecutorTest.java │ │ │ ├── CloudEventsTest.java │ │ │ ├── GcfEventsTest.java │ │ │ ├── HttpFunctionExecutorTest.java │ │ │ ├── IntegrationTest.java │ │ │ ├── TypedFunctionExecutorTest.java │ │ │ ├── http │ │ │ └── HttpTest.java │ │ │ ├── runner │ │ │ └── InvokerTest.java │ │ │ └── testfunctions │ │ │ ├── BackgroundSnoop.java │ │ │ ├── CloudEventSnoop.java │ │ │ ├── Echo.java │ │ │ ├── EchoUrl.java │ │ │ ├── ExceptionBackground.java │ │ │ ├── ExceptionHttp.java │ │ │ ├── HelloWorld.java │ │ │ ├── Log.java │ │ │ ├── Multipart.java │ │ │ ├── Nested.java │ │ │ ├── TimeoutHttp.java │ │ │ ├── Typed.java │ │ │ ├── TypedBackgroundSnoop.java │ │ │ ├── TypedCustomFormat.java │ │ │ └── TypedVoid.java │ │ └── resources │ │ ├── adder_gcf_ga_event.json │ │ ├── firebase-auth-cloudevent-input.json │ │ ├── firebase-auth-legacy-output.json │ │ ├── firebase-auth1.json │ │ ├── firebase-auth2.json │ │ ├── firebase-db1-cloudevent-input.json │ │ ├── firebase-db1-legacy-output.json │ │ ├── firebase-db1.json │ │ ├── firebase-db2-cloudevent-input.json │ │ ├── firebase-db2-legacy-output.json │ │ ├── firebase-db2.json │ │ ├── firestore_complex-cloudevent-input.json │ │ ├── firestore_complex-legacy-output.json │ │ ├── firestore_complex.json │ │ ├── firestore_simple.json │ │ ├── legacy_pubsub.json │ │ ├── legacy_storage_change.json │ │ ├── pubsub_background.json │ │ ├── pubsub_binary.json │ │ ├── pubsub_emulator.json │ │ ├── pubsub_text-cloudevent-input.json │ │ ├── pubsub_text-legacy-output.json │ │ ├── pubsub_text.json │ │ ├── storage-cloudevent-input.json │ │ ├── storage-legacy-output.json │ │ ├── storage.json │ │ └── typed_nameconcat_request.json ├── pom.xml └── testfunction │ ├── pom.xml │ └── src │ └── test │ └── java │ └── com │ └── example │ └── functionjar │ ├── Background.java │ ├── Checker.java │ ├── Foreground.java │ └── Typed.java └── run_conformance_tests.sh /.github/.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | {"functions-framework-api":"1.1.4","invoker":"1.4.1","function-maven-plugin":"0.11.1"} 2 | -------------------------------------------------------------------------------- /.github/blunderbuss.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "separate-pull-requests": true, 3 | "packages": { 4 | "functions-framework-api": { 5 | "release-type": "maven", 6 | "component": "functions-framework-api", 7 | "extra-files": [ 8 | { 9 | "type": "xml", 10 | "path": "pom.xml", 11 | "xpath": "//*[local-name()='artifactId' and text()='functions-framework-api']/parent::*/*[local-name()='version']" 12 | } 13 | ] 14 | }, 15 | "invoker": { 16 | "release-type": "maven", 17 | "component": "java-function-invoker", 18 | "extra-files": [ 19 | { 20 | "type": "xml", 21 | "path": "pom.xml", 22 | "xpath": "//*[local-name()='artifactId' and text()='java-function-invoker-parent']/parent::*/*[local-name()='version']" 23 | }, 24 | { 25 | "type": "xml", 26 | "path": "core/pom.xml", 27 | "xpath": "//*[local-name()='artifactId' and text()='java-function-invoker-parent']/parent::*/*[local-name()='version']" 28 | }, 29 | { 30 | "type": "xml", 31 | "path": "core/pom.xml", 32 | "xpath": "//*[local-name()='artifactId' and text()='java-function-invoker']/parent::*/*[local-name()='version']" 33 | }, 34 | { 35 | "type": "xml", 36 | "path": "core/pom.xml", 37 | "xpath": "//*[local-name()='artifactId' and text()='java-function-invoker-testfunction']/parent::*/*[local-name()='version']" 38 | }, 39 | { 40 | "type": "xml", 41 | "path": "conformance/pom.xml", 42 | "xpath": "//*[local-name()='artifactId' and text()='java-function-invoker-parent']/parent::*/*[local-name()='version']" 43 | }, 44 | { 45 | "type": "xml", 46 | "path": "conformance/pom.xml", 47 | "xpath": "//*[local-name()='artifactId' and text()='conformance']/parent::*/*[local-name()='version']" 48 | }, 49 | { 50 | "type": "xml", 51 | "path": "testfunction/pom.xml", 52 | "xpath": "//*[local-name()='artifactId' and text()='java-function-invoker-parent']/parent::*/*[local-name()='version']" 53 | }, 54 | { 55 | "type": "xml", 56 | "path": "testfunction/pom.xml", 57 | "xpath": "//*[local-name()='artifactId' and text()='java-function-invoker-testfunction']/parent::*/*[local-name()='version']" 58 | } 59 | ] 60 | }, 61 | "function-maven-plugin": { 62 | "release-type": "maven", 63 | "component": "function-maven-plugin", 64 | "extra-files": [ 65 | { 66 | "type": "xml", 67 | "path": "pom.xml", 68 | "xpath": "//*[local-name()='artifactId' and text()='function-maven-plugin']/parent::*/*[local-name()='version']" 69 | } 70 | ] 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/release-please.yml: -------------------------------------------------------------------------------- 1 | handleGHRelease: true 2 | monorepoTags: true 3 | manifest: true 4 | manifestConfig: '.github/release-please-config.json' 5 | manifestFile: '.github/.release-please-manifest.json' -------------------------------------------------------------------------------- /.github/release-trigger.yml: -------------------------------------------------------------------------------- 1 | enabled: true 2 | multiScmName: functions-framework-java 3 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["group:allNonMajor", "schedule:monthly"], 4 | "ignoreDeps": ["org.apache.maven.plugins:maven-source-plugin"], 5 | "packageRules": [ 6 | { 7 | "description": "Create a PR whenever there is a new major version", 8 | "matchUpdateTypes": [ 9 | "major" 10 | ] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/buildpack-integration-test.yml: -------------------------------------------------------------------------------- 1 | # Validates Functions Framework with GCF buildpacks. 2 | name: Buildpack Integration Test 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | # Runs every day on 12:00 AM PST 10 | schedule: 11 | - cron: "0 0 * * *" 12 | 13 | # Declare default permissions as read only. 14 | permissions: read-all 15 | 16 | jobs: 17 | java11-buildpack-test: 18 | uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main 19 | with: 20 | http-builder-source: '/tmp/tests/conformance' 21 | http-builder-target: 'com.google.cloud.functions.conformance.HttpConformanceFunction' 22 | cloudevent-builder-source: '/tmp/tests/conformance' 23 | cloudevent-builder-target: 'com.google.cloud.functions.conformance.CloudEventsConformanceFunction' 24 | prerun: 'invoker/conformance/prerun.sh' 25 | builder-runtime: 'java11' 26 | builder-runtime-version: '11' 27 | builder-url: gcr.io/serverless-runtimes/google-22-full/builder/java:latest 28 | java17-buildpack-test: 29 | uses: GoogleCloudPlatform/functions-framework-conformance/.github/workflows/buildpack-integration-test.yml@main 30 | with: 31 | http-builder-source: '/tmp/tests/conformance' 32 | http-builder-target: 'com.google.cloud.functions.conformance.HttpConformanceFunction' 33 | cloudevent-builder-source: '/tmp/tests/conformance' 34 | cloudevent-builder-target: 'com.google.cloud.functions.conformance.CloudEventsConformanceFunction' 35 | prerun: 'invoker/conformance/prerun.sh' 36 | builder-runtime: 'java17' 37 | builder-runtime-version: '17' 38 | builder-url: gcr.io/serverless-runtimes/google-22-full/builder/java:latest 39 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [ "main" ] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | # Autobuild each of these seperate maven projects 27 | working-directory: ['invoker', 'functions-framework-api', 'function-maven-plugin'] 28 | 29 | steps: 30 | - name: Harden Runner 31 | uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 32 | with: 33 | disable-sudo: true 34 | egress-policy: block 35 | allowed-endpoints: > 36 | api.github.com:443 37 | github.com:443 38 | objects.githubusercontent.com:443 39 | proxy.golang.org:443 40 | repo.maven.apache.org:443 41 | storage.googleapis.com:443 42 | uploads.github.com:443 43 | 44 | - name: Checkout repository 45 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 46 | 47 | # Initializes the CodeQL tools for scanning. 48 | - name: Initialize CodeQL 49 | uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 50 | with: 51 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 52 | languages: java 53 | # If you wish to specify custom queries, you can do so here or in a config file. 54 | # By default, queries listed here will override any specified in a config file. 55 | # Prefix the list here with "+" to use these queries and those in the config file. 56 | 57 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 58 | # queries: security-extended,security-and-quality 59 | 60 | 61 | 62 | - name: Build 63 | run: | 64 | (cd functions-framework-api/ && mvn install) 65 | (cd invoker/ && mvn clean install) 66 | (cd function-maven-plugin && mvn install) 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 70 | with: 71 | category: ${{ matrix.working-directory }} 72 | -------------------------------------------------------------------------------- /.github/workflows/conformance.yaml: -------------------------------------------------------------------------------- 1 | name: Java Conformance CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | 8 | # Declare default permissions as read only. 9 | permissions: read-all 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | java: [ 17 | 11.x, 18 | ] 19 | steps: 20 | - name: Harden Runner 21 | uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 22 | with: 23 | disable-sudo: true 24 | egress-policy: block 25 | allowed-endpoints: > 26 | api.github.com:443 27 | github.com:443 28 | objects.githubusercontent.com:443 29 | proxy.golang.org:443 30 | repo.maven.apache.org:443 31 | storage.googleapis.com:443 32 | 33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 34 | 35 | - name: Set up JDK ${{ matrix.java }} 36 | uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 37 | with: 38 | java-version: ${{ matrix.java }} 39 | distribution: temurin 40 | 41 | - name: Setup Go 42 | uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0 43 | with: 44 | go-version: '1.21' 45 | 46 | - name: Build API with Maven 47 | run: (cd functions-framework-api/ && mvn install) 48 | 49 | - name: Build invoker with Maven 50 | run: (cd invoker/ && mvn install) 51 | 52 | - name: Build invoker Maven Plugin 53 | run: (cd function-maven-plugin/ && mvn install) 54 | 55 | - name: Run HTTP conformance tests 56 | uses: GoogleCloudPlatform/functions-framework-conformance/action@1041a97e93a463d9efb17dda821f3ddc0bf0024f # main 57 | with: 58 | functionType: 'http' 59 | useBuildpacks: false 60 | cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.HttpConformanceFunction'" 61 | startDelay: 10 62 | 63 | - name: Run Typed conformance tests 64 | uses: GoogleCloudPlatform/functions-framework-conformance/action@1041a97e93a463d9efb17dda821f3ddc0bf0024f # main 65 | with: 66 | functionType: 'http' 67 | declarativeType: 'typed' 68 | useBuildpacks: false 69 | cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.TypedConformanceFunction'" 70 | startDelay: 10 71 | 72 | - name: Run background event conformance tests 73 | uses: GoogleCloudPlatform/functions-framework-conformance/action@1041a97e93a463d9efb17dda821f3ddc0bf0024f # main 74 | with: 75 | functionType: 'legacyevent' 76 | useBuildpacks: false 77 | validateMapping: true 78 | cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.BackgroundEventConformanceFunction'" 79 | startDelay: 10 80 | 81 | - name: Run cloudevent conformance tests 82 | uses: GoogleCloudPlatform/functions-framework-conformance/action@1041a97e93a463d9efb17dda821f3ddc0bf0024f # main 83 | with: 84 | functionType: 'cloudevent' 85 | useBuildpacks: false 86 | validateMapping: true 87 | cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.CloudEventsConformanceFunction'" 88 | startDelay: 10 89 | 90 | - name: Run HTTP concurrency conformance tests 91 | uses: GoogleCloudPlatform/functions-framework-conformance/action@1041a97e93a463d9efb17dda821f3ddc0bf0024f # main 92 | with: 93 | functionType: 'http' 94 | useBuildpacks: false 95 | validateConcurrency: true 96 | cmd: "'mvn -f invoker/conformance/pom.xml function:run -Drun.functionTarget=com.google.cloud.functions.conformance.ConcurrentHttpConformanceFunction'" 97 | startDelay: 10 -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Java Lint CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | workflow_dispatch: 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Harden Runner 16 | uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 17 | with: 18 | disable-sudo: true 19 | egress-policy: block 20 | allowed-endpoints: > 21 | github.com:443 22 | repo.maven.apache.org:443 23 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 24 | - name: Set up JDK 25 | uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 26 | with: 27 | java-version: 11.x 28 | distribution: temurin 29 | - name: Build API with Maven 30 | run: (cd functions-framework-api/ && mvn install) 31 | - name: Lint Functions Framework API 32 | run: (cd functions-framework-api/ && mvn clean verify -DskipTests -P lint) 33 | - name: Build Invoker with Maven 34 | run: (cd functions-framework-api/ && mvn install) 35 | - name: Lint Invoker 36 | run: (cd invoker/ && mvn clean verify -DskipTests -P lint) 37 | formatting: 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Harden Runner 41 | uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 42 | with: 43 | egress-policy: audit # TODO: change to 'egress-policy: block' after couple of runs 44 | 45 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # v2 minimum required 46 | - name: Set up JDK 47 | uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 48 | with: 49 | java-version: 17.x 50 | distribution: temurin 51 | - name: Run formatter 52 | id: formatter 53 | uses: axel-op/googlejavaformat-action@dbff853fb823671ec5781365233bf86543b13215 # v3 54 | with: 55 | args: "--replace" 56 | skip-commit: true 57 | - name: Print diffs 58 | run: git --no-pager diff --exit-code 59 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | name: Scorecard supply-chain security 2 | on: 3 | # For Branch-Protection check. Only the default branch is supported. See 4 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 5 | branch_protection_rule: 6 | # To guarantee Maintained check is occasionally updated. See 7 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 8 | schedule: 9 | - cron: '0 */12 * * *' 10 | push: 11 | branches: [ "main" ] 12 | workflow_dispatch: 13 | 14 | # Declare default permissions as read only. 15 | permissions: read-all 16 | 17 | jobs: 18 | analysis: 19 | name: Scorecard analysis 20 | runs-on: ubuntu-latest 21 | permissions: 22 | # Needed to upload the results to code-scanning dashboard. 23 | security-events: write 24 | # Needed to publish results and get a badge (see publish_results below). 25 | id-token: write 26 | 27 | steps: 28 | - name: Harden Runner 29 | uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 30 | with: 31 | disable-sudo: true 32 | egress-policy: block 33 | allowed-endpoints: > 34 | api.osv.dev:443 35 | api.scorecard.dev:443 36 | api.securityscorecards.dev:443 37 | auth.docker.io:443 38 | bestpractices.coreinfrastructure.org:443 39 | github.com:443 40 | index.docker.io:443 41 | oss-fuzz-build-logs.storage.googleapis.com:443 42 | sigstore-tuf-root.storage.googleapis.com:443 43 | www.bestpractices.dev:443 44 | *.sigstore.dev:443 45 | *.github.com:443 46 | 47 | - name: "Checkout code" 48 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 49 | with: 50 | persist-credentials: false 51 | 52 | - name: "Run analysis" 53 | uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # v2.4.1 54 | with: 55 | results_file: results.sarif 56 | results_format: sarif 57 | # Public repositories: 58 | # - Publish results to OpenSSF REST API for easy access by consumers 59 | # - Allows the repository to include the Scorecard badge. 60 | # - See https://github.com/ossf/scorecard-action#publishing-results. 61 | publish_results: true 62 | 63 | # Upload the results to GitHub's code scanning dashboard. 64 | - name: "Upload to code-scanning" 65 | uses: github/codeql-action/upload-sarif@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 66 | with: 67 | sarif_file: results.sarif 68 | -------------------------------------------------------------------------------- /.github/workflows/unit.yaml: -------------------------------------------------------------------------------- 1 | name: Java Unit CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | java: [ 16 | 11.x, 17 | 17.x, 18 | 21-ea 19 | ] 20 | steps: 21 | - name: Harden Runner 22 | uses: step-security/harden-runner@4d991eb9b905ef189e4c376166672c3f2f230481 # v2.11.0 23 | with: 24 | disable-sudo: true 25 | egress-policy: block 26 | allowed-endpoints: > 27 | github.com:443 28 | repo.maven.apache.org:443 29 | api.adoptium.net:443 30 | *.githubusercontent.com:443 31 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 32 | - name: Set up JDK ${{ matrix.java }} 33 | uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0 34 | with: 35 | java-version: ${{ matrix.java }} 36 | distribution: temurin 37 | - name: Build with Maven 38 | run: (cd functions-framework-api/ && mvn install) 39 | - name: Test 40 | run: (cd invoker/ && mvn test) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # Maven 22 | target/ 23 | dependency-reduced-pom.xml 24 | 25 | # Gradle 26 | .gradle 27 | .idea/ 28 | build/ 29 | credentials/ 30 | out/ 31 | gradlew 32 | gradlew.bat 33 | *.iml 34 | *.properties 35 | 36 | client_secret.json 37 | credentials.json 38 | tokens/ 39 | 40 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 41 | hs_err_pid* 42 | 43 | .DS_Store 44 | .settings/ 45 | .classpath 46 | .project 47 | properties 48 | 49 | # IDE 50 | .vscode/ 51 | 52 | # Conformance testing 53 | function_output.json 54 | serverlog_stderr.txt 55 | serverlog_stdout.txt 56 | -------------------------------------------------------------------------------- /.kokoro/release.cfg: -------------------------------------------------------------------------------- 1 | build_file: "functions-framework-java/.kokoro/release.sh" 2 | 3 | before_action { 4 | fetch_keystore { 5 | keystore_resource { 6 | keystore_config_id: 75669 7 | keyname: "functions-framework-java-release-bot-sonatype-password" 8 | } 9 | keystore_resource { 10 | keystore_config_id: 70247 11 | keyname: "maven-gpg-pubkeyring" 12 | } 13 | keystore_resource { 14 | keystore_config_id: 70247 15 | keyname: "maven-gpg-keyring" 16 | } 17 | keystore_resource { 18 | keystore_config_id: 70247 19 | keyname: "maven-gpg-passphrase" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.kokoro/release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Stop execution when any command fails. 4 | set -e 5 | 6 | # update the Maven version to 3.6.3 7 | pushd /usr/local 8 | wget https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.tar.gz 9 | tar -xvzf apache-maven-3.6.3-bin.tar.gz apache-maven-3.6.3 10 | rm -f /usr/local/apache-maven 11 | ln -s /usr/local/apache-maven-3.6.3 /usr/local/apache-maven 12 | rm apache-maven-3.6.3-bin.tar.gz 13 | popd 14 | 15 | 16 | # Get secrets from keystore and set and environment variables. 17 | setup_environment_secrets() { 18 | export GPG_TTY=$(tty) 19 | export SONATYPE_USERNAME=$(cat ${KOKORO_KEYSTORE_DIR}/75669_functions-framework-java-release-bot-sonatype-password | cut -f1 -d':') 20 | export SONATYPE_PASSWORD=$(cat ${KOKORO_KEYSTORE_DIR}/75669_functions-framework-java-release-bot-sonatype-password | cut -f2 -d':') 21 | export GPG_PASSPHRASE=$(cat ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-passphrase) 22 | 23 | # Add the key ring files to $GNUPGHOME to verify the GPG credentials. 24 | export GNUPGHOME=/tmp/gpg 25 | mkdir $GNUPGHOME 26 | mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-pubkeyring $GNUPGHOME/pubring.gpg 27 | mv ${KOKORO_KEYSTORE_DIR}/70247_maven-gpg-keyring $GNUPGHOME/secring.gpg 28 | gpg -k 29 | } 30 | 31 | create_settings_xml_file() { 32 | echo " 33 | 34 | 35 | 36 | true 37 | 38 | 39 | ${GPG_PASSPHRASE} 40 | 41 | 42 | 43 | 44 | 45 | sonatype-nexus-staging 46 | ${SONATYPE_USERNAME} 47 | ${SONATYPE_PASSWORD} 48 | 49 | 50 | sonatype-nexus-snapshots 51 | ${SONATYPE_USERNAME} 52 | ${SONATYPE_PASSWORD} 53 | 54 | 55 | " > $1 56 | } 57 | 58 | setup_environment_secrets 59 | 60 | # Pick the right package to release based on the Kokoro job name. 61 | cd ${KOKORO_ARTIFACTS_DIR}/github/functions-framework-java 62 | create_settings_xml_file "settings.xml" 63 | echo "KOKORO_JOB_NAME=${KOKORO_JOB_NAME}" 64 | if [[ $KOKORO_JOB_NAME == *"function-maven-plugin"* ]]; then 65 | cd function-maven-plugin 66 | elif [[ $KOKORO_JOB_NAME == *"functions-framework-api"* ]]; then 67 | cd functions-framework-api 68 | else 69 | cd invoker 70 | fi 71 | echo "pwd=$(pwd)" 72 | 73 | # Make sure `JAVA_HOME` is set and using jdk11. 74 | export JAVA_HOME=/usr/lib/jvm/java-1.11.0-openjdk-amd64 75 | echo "JAVA_HOME=$JAVA_HOME" 76 | mvn clean deploy -B \ 77 | -P sonatype-oss-release \ 78 | --settings=../settings.xml \ 79 | -Dgpg.executable=gpg \ 80 | -Dgpg.passphrase=${GPG_PASSPHRASE} \ 81 | -Dgpg.homedir=${GNUPGHOME} 82 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | 30 | ## Developing 31 | 32 | This project is divided into multiple packages, primarily: 33 | 34 | - [`functions-framework-api`](./functions-framework-api) – The interfaces for functions. 35 | - [`java-function-invoker`](./invoker) 36 | - `core` - The function invoker 37 | - `testfunction` - A set of test functions 38 | - `function-maven-plugin` - The Maven plugin for building functions 39 | - `conformance` - A set of functions used for conformance testing 40 | 41 | ### Setup JDK 11 / 17 42 | 43 | Install JDK 11 and 17. One way to install these is through [SDK man](https://sdkman.io/). 44 | 45 | ```sh 46 | sdk install java 11.0.2-open 47 | sdk install java 17-open 48 | sdk use java 17-open 49 | sdk use java 11.0.2-open 50 | ``` 51 | 52 | Verify Java version with: 53 | 54 | ```sh 55 | java --version 56 | ``` 57 | 58 | ### Setup Apache Maven 59 | 60 | Install `mvn`: 61 | 62 | https://maven.apache.org/install.html 63 | 64 | ### Formatting 65 | This repo follows the Google Java Style guide for formatting. You can setup the 66 | formatting tool locally using one of the options provided at 67 | [google/google-java-format](https://github.com/google/google-java-format#google-java-format). 68 | 69 | ## Install and Run Invoker Tests Locally 70 | 71 | ``` 72 | cd invoker; 73 | mvn test; 74 | ``` 75 | 76 | ### Running Conformance Tests Locally 77 | 78 | First, install Go 1.16+, then run the conformance tests with this script: 79 | 80 | ``` 81 | ./run_conformance_tests.sh 82 | ``` 83 | -------------------------------------------------------------------------------- /function-maven-plugin/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.11.1](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/function-maven-plugin-v0.11.0...function-maven-plugin-v0.11.1) (2024-11-27) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * revert maven-source-plugin to 3.2.1 ([#303](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/303)) ([2db9a2b](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/2db9a2bec6ba93e7954e68c2301c5fc2fcc032d8)) 9 | 10 | ## [0.11.0](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/function-maven-plugin-v0.10.1...function-maven-plugin-v0.11.0) (2023-05-31) 11 | 12 | 13 | ### Features 14 | 15 | * Add --gen2 support ([#172](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/172)) ([3b7b701](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/3b7b70152ca614e2a3b52f1a7c07d89221095a7d)) 16 | * Define strongly typed function interface ([#186](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/186)) ([5264e35](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/5264e35b2522a789d65f0e0fd9bb5584694529eb)) 17 | -------------------------------------------------------------------------------- /function-maven-plugin/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | org.sonatype.oss 6 | oss-parent 7 | 9 8 | 9 | 10 | com.google.cloud.functions 11 | function-maven-plugin 12 | maven-plugin 13 | 0.11.2-SNAPSHOT 14 | Functions Framework Plugin 15 | A Maven plugin that allows functions to be deployed, and to be run locally 16 | using the Java Functions Framework. 17 | http://maven.apache.org 18 | 19 | 20 | http://github.com/GoogleCloudPlatform/functions-framework-java 21 | scm:git:git://github.com/GoogleCloudPlatform/functions-framework-java.git 22 | scm:git:ssh://git@github.com/GoogleCloudPlatform/functions-framework-java.git 23 | HEAD 24 | 25 | 26 | 27 | 28 | Apache License, Version 2.0 29 | http://www.apache.org/licenses/LICENSE-2.0.txt 30 | repo 31 | 32 | 33 | 34 | 35 | 11 36 | 11 37 | 38 | 39 | 40 | 41 | 42 | org.apache.maven 43 | maven-plugin-api 44 | 3.9.9 45 | 46 | 47 | org.apache.maven 48 | maven-core 49 | 3.9.9 50 | 51 | 52 | org.apache.maven.plugin-tools 53 | maven-plugin-annotations 54 | 3.15.1 55 | provided 56 | 57 | 58 | 59 | com.google.cloud.functions.invoker 60 | java-function-invoker 61 | 1.4.0 62 | 63 | 64 | 65 | com.google.cloud.tools 66 | appengine-maven-plugin 67 | 2.8.3 68 | jar 69 | 70 | 71 | 72 | com.google.truth 73 | truth 74 | 1.4.4 75 | test 76 | 77 | 78 | junit 79 | junit 80 | 4.13.2 81 | test 82 | 83 | 84 | 85 | 86 | 87 | 88 | org.apache.maven.plugins 89 | maven-plugin-plugin 90 | 3.15.1 91 | 92 | 93 | help-goal 94 | 95 | helpmojo 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | sonatype-nexus-snapshots 105 | Sonatype Nexus Snapshots 106 | https://oss.sonatype.org/content/repositories/snapshots/ 107 | 108 | 109 | sonatype-nexus-staging 110 | Nexus Release Repository 111 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 112 | 113 | 114 | 115 | 116 | sonatype-oss-release 117 | 118 | 119 | 120 | org.apache.maven.plugins 121 | maven-source-plugin 122 | 3.2.1 123 | 124 | 125 | attach-sources 126 | 127 | jar-no-fork 128 | 129 | 130 | 131 | 132 | 133 | org.apache.maven.plugins 134 | maven-javadoc-plugin 135 | 3.11.2 136 | 137 | 138 | attach-javadocs 139 | 140 | jar 141 | 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-gpg-plugin 148 | 3.2.7 149 | 150 | 151 | sign-artifacts 152 | verify 153 | 154 | sign 155 | 156 | 157 | 158 | 159 | 160 | org.sonatype.plugins 161 | nexus-staging-maven-plugin 162 | 1.7.0 163 | true 164 | 165 | sonatype-nexus-snapshots 166 | https://oss.sonatype.org/ 167 | true 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /function-maven-plugin/src/main/java/com/google/cloud/functions/plugin/RunFunction.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.plugin; 2 | 3 | import com.google.cloud.functions.invoker.runner.Invoker; 4 | import java.io.File; 5 | import java.util.ArrayList; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import org.apache.maven.plugin.AbstractMojo; 9 | import org.apache.maven.plugin.MojoExecutionException; 10 | import org.apache.maven.plugins.annotations.Execute; 11 | import org.apache.maven.plugins.annotations.LifecyclePhase; 12 | import org.apache.maven.plugins.annotations.Mojo; 13 | import org.apache.maven.plugins.annotations.Parameter; 14 | import org.apache.maven.plugins.annotations.ResolutionScope; 15 | 16 | /** 17 | * Runs a function using the Java Functions Framework. Typically this plugin is configured in one of 18 | * two ways. Either in the pom.xml file, like this... 19 | * 20 | *
{@code
21 |  * 
22 |  *   com.google.cloud.functions
23 |  *   function-maven-plugin
24 |  *   1.0.0-alpha-2-rc3
25 |  *   
26 |  *     com.example.function.Echo
27 |  *   
28 |  * 
29 |  * }
30 | * 31 | * ...and then run using {@code mvn function:run}. Or using properties on the command line, like 32 | * this...
33 | * 34 | *
{@code
35 |  * mvn com.google.cloud.functions:function:1.0.0-alpha-2-rc3:run \
36 |  *     -Drun.functionTarget=com.example.function.Echo
37 |  * }
38 | */ 39 | @Mojo( 40 | name = "run", 41 | defaultPhase = LifecyclePhase.GENERATE_RESOURCES, 42 | requiresDependencyResolution = ResolutionScope.RUNTIME, 43 | requiresDependencyCollection = ResolutionScope.RUNTIME) 44 | @Execute(phase = LifecyclePhase.COMPILE) 45 | public class RunFunction extends AbstractMojo { 46 | 47 | /** 48 | * The name of the function to run. This is the name of a class that implements one of the 49 | * interfaces in {@code com.google.cloud.functions}. 50 | */ 51 | @Parameter(property = "run.functionTarget") 52 | private String functionTarget; 53 | 54 | /** The port on which the HTTP server wrapping the function should listen. */ 55 | @Parameter(property = "run.port", defaultValue = "8080") 56 | private Integer port; 57 | 58 | /** 59 | * Used to determine what classpath needs to be used to load the function. This parameter is 60 | * injected by Maven and can't be set explicitly in a pom.xml file. 61 | */ 62 | @Parameter(defaultValue = "${project.runtimeClasspathElements}", readonly = true, required = true) 63 | private List runtimePath; 64 | 65 | public void execute() throws MojoExecutionException { 66 | String classpath = String.join(File.pathSeparator, runtimePath); 67 | List args = new ArrayList<>(); 68 | args.addAll(Arrays.asList("--classpath", classpath)); 69 | if (functionTarget != null) { 70 | args.addAll(Arrays.asList("--target", functionTarget)); 71 | } 72 | if (port != null) { 73 | args.addAll(Arrays.asList("--port", String.valueOf(port))); 74 | } 75 | try { 76 | getLog().info("Calling Invoker with " + args); 77 | Invoker.main(args.toArray(new String[0])); 78 | } catch (Exception e) { 79 | getLog().error("Could not invoke function: " + e, e); 80 | throw new MojoExecutionException("Could not invoke function", e); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /function-maven-plugin/src/test/java/com/google/cloud/functions/plugin/DeployFunctionTest.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.plugin; 2 | 3 | import static com.google.common.truth.Truth.assertThat; 4 | 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.common.collect.ImmutableMap; 7 | import java.util.List; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.junit.runners.JUnit4; 11 | 12 | @RunWith(JUnit4.class) 13 | public class DeployFunctionTest { 14 | 15 | @Test 16 | public void testDeployFunctionCommandLine() { 17 | DeployFunction mojo = new DeployFunction(); 18 | mojo.envVarsFile = "myfile"; 19 | mojo.buildEnvVarsFile = "myfile2"; 20 | mojo.functionTarget = "function"; 21 | mojo.ignoreFile = "ff"; 22 | mojo.maxInstances = new Integer(3); 23 | mojo.memory = "234"; 24 | mojo.name = "a name"; 25 | mojo.region = "a region"; 26 | mojo.retry = "44"; 27 | mojo.source = "a source"; 28 | mojo.stageBucket = "a bucket"; 29 | mojo.timeout = "timeout"; 30 | mojo.vpcConnector = "a connector"; 31 | mojo.triggerHttp = true; 32 | mojo.allowUnauthenticated = true; 33 | mojo.environmentVariables = ImmutableMap.of("env1", "a", "env2", "b"); 34 | mojo.buildEnvironmentVariables = ImmutableMap.of("env1", "a", "env2", "b"); 35 | List expected = 36 | ImmutableList.of( 37 | "functions", 38 | "deploy", 39 | "a name", 40 | "--region=a region", 41 | "--trigger-http", 42 | "--allow-unauthenticated", 43 | "--entry-point=function", 44 | "--ignore-file=ff", 45 | "--memory=234", 46 | "--retry=44", 47 | "--source=a source", 48 | "--stage-bucket=a bucket", 49 | "--timeout=timeout", 50 | "--vpc-connector=a connector", 51 | "--max-instances=3", 52 | "--set-env-vars=env1=a,env2=b", 53 | "--env-vars-file=myfile", 54 | "--set-build-env-vars=env1=a,env2=b", 55 | "--build-env-vars-file=myfile2", 56 | "--runtime=java11"); 57 | assertThat(mojo.getCommands()).isEqualTo(expected); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /functions-framework-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.4](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/functions-framework-api-v1.1.3...functions-framework-api-v1.1.4) (2024-11-22) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * revert maven-source-plugin to 3.2.1 ([#303](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/303)) ([2db9a2b](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/2db9a2bec6ba93e7954e68c2301c5fc2fcc032d8)) 9 | 10 | ## [1.1.3](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/functions-framework-api-v1.1.2...functions-framework-api-v1.1.3) (2024-10-05) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * revert maven-source-plugin to 3.2.1 ([#297](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/297)) ([8f1fd84](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/8f1fd84ca4cc43b2e93b66fe160f78a868b55ffe)) 16 | 17 | ## [1.1.2](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/functions-framework-api-v1.1.1...functions-framework-api-v1.1.2) (2024-09-27) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * use a version of maven-source-plugin that's available in mavencentral ([#288](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/288)) ([f8c1d57](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/f8c1d575660312101532a1f579c0492593248f37)) 23 | 24 | ## [1.1.1](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/functions-framework-api-v1.1.0...functions-framework-api-v1.1.1) (2024-06-27) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * release 1.1.1; this updates transitive dependencies on jackson ([#277](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/277)) ([7e4ca5d](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/7e4ca5d15d5b200787b999f82da6d6cd1cbd4b7e)) 30 | 31 | ## [1.1.0](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/functions-framework-api-v1.0.4...functions-framework-api-v1.1.0) (2023-05-31) 32 | 33 | 34 | ### Features 35 | 36 | * Define strongly typed function interface ([#186](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/186)) ([5264e35](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/5264e35b2522a789d65f0e0fd9bb5584694529eb)) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * remove warnings from mvn install ([#66](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/66)) ([270f4ec](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/270f4ec7936239eff9c00b8d3ff0f09a8615b9c9)) 42 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/BackgroundFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | /** 18 | * Represents a Cloud Function that is activated by an event and parsed into a user-supplied class. 19 | * The payload of the event is a JSON object, which is deserialized into a user-defined class as 20 | * described for Gson. 22 | * 23 | *

Here is an example of an implementation that accesses the {@code messageId} property from a 24 | * payload that matches a user-defined {@code PubSubMessage} class: 25 | * 27 | * 28 | *

29 |  * public class Example implements{@code BackgroundFunction} {
30 |  *   private static final Logger logger = Logger.getLogger(Example.class.getName());
31 |  *
32 |  *  {@code @Override}
33 |  *   public void accept(PubSubMessage pubSubMessage, Context context) {
34 |  *     logger.info("Got messageId " + pubSubMessage.messageId);
35 |  *   }
36 |  * }
37 |  *
38 |  * // Where PubSubMessage is a user-defined class like this:
39 |  * public class PubSubMessage {
40 |  *   String data;
41 |  *  {@code Map} attributes;
42 |  *   String messageId;
43 |  *   String publishTime;
44 |  * }
45 |  * 
46 | * 47 | * @param the class of payload objects that this function expects. 48 | */ 49 | @FunctionalInterface 50 | public interface BackgroundFunction { 51 | /** 52 | * Called to service an incoming event. This interface is implemented by user code to provide the 53 | * action for a given background function. If this method throws any exception (including any 54 | * {@link Error}) then the HTTP response will have a 500 status code. 55 | * 56 | * @param payload the payload of the event, deserialized from the original JSON string. 57 | * @param context the context of the event. This is a set of values that every event has, 58 | * separately from the payload, such as timestamp and event type. 59 | * @throws Exception to produce a 500 status code in the HTTP response. 60 | */ 61 | void accept(T payload, Context context) throws Exception; 62 | } 63 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/CloudEventsFunction.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions; 2 | 3 | import io.cloudevents.CloudEvent; 4 | 5 | /** 6 | * Represents a Cloud Function that is activated by an event and parsed into a {@link CloudEvent} 7 | * object. 8 | */ 9 | @FunctionalInterface 10 | public interface CloudEventsFunction { 11 | /** 12 | * Called to service an incoming event. This interface is implemented by user code to provide the 13 | * action for a given background function. If this method throws any exception (including any 14 | * {@link Error}) then the HTTP response will have a 500 status code. 15 | * 16 | * @param event the event. 17 | * @throws Exception to produce a 500 status code in the HTTP response. 18 | */ 19 | void accept(CloudEvent event) throws Exception; 20 | } 21 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/Context.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | import java.util.Collections; 18 | import java.util.Map; 19 | 20 | /** An interface for event function context. */ 21 | public interface Context { 22 | /** 23 | * Returns event ID. 24 | * 25 | * @return event ID 26 | */ 27 | String eventId(); 28 | 29 | /** 30 | * Returns event timestamp. 31 | * 32 | * @return event timestamp 33 | */ 34 | String timestamp(); 35 | 36 | /** 37 | * Returns event type. 38 | * 39 | * @return event type 40 | */ 41 | String eventType(); 42 | 43 | /** 44 | * Returns event resource. 45 | * 46 | * @return event resource 47 | */ 48 | String resource(); 49 | 50 | /** 51 | * Returns additional attributes from this event. For CloudEvents, the entries in this map will 52 | * include the required 54 | * attributes and may include optional 56 | * attributes and 58 | * extension attributes. 59 | * 60 | *

The map returned by this method may be empty but is never null. 61 | * 62 | * @return additional attributes form this event. 63 | */ 64 | default Map attributes() { 65 | return Collections.emptyMap(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/HttpFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | /** Represents a Cloud Function that is activated by an HTTP request. */ 18 | @FunctionalInterface 19 | public interface HttpFunction { 20 | /** 21 | * Called to service an incoming HTTP request. This interface is implemented by user code to 22 | * provide the action for a given function. If the method throws any exception (including any 23 | * {@link Error}) then the HTTP response will have a 500 status code. 24 | * 25 | * @param request a representation of the incoming HTTP request. 26 | * @param response an object that can be used to provide the corresponding HTTP response. 27 | * @throws Exception if thrown, the HTTP response will have a 500 status code. 28 | */ 29 | void service(HttpRequest request, HttpResponse response) throws Exception; 30 | } 31 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/HttpMessage.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.Optional; 23 | 24 | /** Represents an HTTP message, either an HTTP request or a part of a multipart HTTP request. */ 25 | public interface HttpMessage { 26 | /** 27 | * Returns the value of the {@code Content-Type} header, if any. 28 | * 29 | * @return the content type, if any. 30 | */ 31 | Optional getContentType(); 32 | 33 | /** 34 | * Returns the numeric value of the {@code Content-Length} header. 35 | * 36 | * @return the content length. 37 | */ 38 | long getContentLength(); 39 | 40 | /** 41 | * Returns the character encoding specified in the {@code Content-Type} header, or {@code 42 | * Optional.empty()} if there is no {@code Content-Type} header or it does not have the {@code 43 | * charset} parameter. 44 | * 45 | * @return the character encoding for the content type, if one is specified. 46 | */ 47 | Optional getCharacterEncoding(); 48 | 49 | /** 50 | * Returns an {@link InputStream} that can be used to read the body of this HTTP request. Every 51 | * call to this method on the same {@link HttpMessage} will return the same object. This method is 52 | * typically used to read binary data. If the body is text, the {@link #getReader()} method is 53 | * more appropriate. 54 | * 55 | * @return an {@link InputStream} that can be used to read the body of this HTTP request. 56 | * @throws IOException if a valid {@link InputStream} cannot be returned for some reason. 57 | * @throws IllegalStateException if {@link #getReader()} has already been called on this instance. 58 | */ 59 | InputStream getInputStream() throws IOException; 60 | 61 | /** 62 | * Returns a {@link BufferedReader} that can be used to read the text body of this HTTP request. 63 | * Every call to this method on the same {@link HttpMessage} will return the same object. 64 | * 65 | * @return a {@link BufferedReader} that can be used to read the text body of this HTTP request. 66 | * @throws IOException if a valid {@link BufferedReader} cannot be returned for some reason. 67 | * @throws IllegalStateException if {@link #getInputStream()} has already been called on this 68 | * instance. 69 | */ 70 | BufferedReader getReader() throws IOException; 71 | 72 | /** 73 | * Returns a map describing the headers of this HTTP request, or this part of a multipart request. 74 | * If the headers look like this... 75 | * 76 | *

 77 |    *   Content-Type: text/plain
 78 |    *   Some-Header: some value
 79 |    *   Some-Header: another value
 80 |    * 
81 | * 82 | * ...then the returned value will map {@code "Content-Type"} to a one-element list containing 83 | * {@code "text/plain"}, and {@code "Some-Header"} to a two-element list containing {@code "some 84 | * value"} and {@code "another value"}. 85 | * 86 | * @return a map where each key is an HTTP header and the corresponding {@code List} value has one 87 | * element for each occurrence of that header. 88 | */ 89 | Map> getHeaders(); 90 | 91 | /** 92 | * Convenience method that returns the value of the first header with the given name. If the 93 | * headers look like this... 94 | * 95 | *
 96 |    *   Content-Type: text/plain
 97 |    *   Some-Header: some value
 98 |    *   Some-Header: another value
 99 |    * 
100 | * 101 | * ...then {@code getFirstHeader("Some-Header")} will return {@code Optional.of("some value")}, 102 | * and {@code getFirstHeader("Another-Header")} will return {@code Optional.empty()}. 103 | * 104 | * @param name an HTTP header name. 105 | * @return the first value of the given header, if present. 106 | */ 107 | default Optional getFirstHeader(String name) { 108 | List headers = getHeaders().get(name); 109 | if (headers == null || headers.isEmpty()) { 110 | return Optional.empty(); 111 | } 112 | return Optional.of(headers.get(0)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/HttpRequest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.Optional; 20 | 21 | /** Represents the contents of an HTTP request that is being serviced by a Cloud Function. */ 22 | public interface HttpRequest extends HttpMessage { 23 | /** 24 | * The HTTP method of this request, such as {@code "POST"} or {@code "GET"}. 25 | * 26 | * @return the HTTP method of this request. 27 | */ 28 | String getMethod(); 29 | 30 | /** 31 | * The full URI of this request as it arrived at the server. 32 | * 33 | * @return the full URI of this request. 34 | */ 35 | String getUri(); 36 | 37 | /** 38 | * The path part of the URI for this request, without any query. If the full URI is {@code 39 | * http://foo.com/bar/baz?this=that}, then this method will return {@code /bar/baz}. 40 | * 41 | * @return the path part of the URI for this request. 42 | */ 43 | String getPath(); 44 | 45 | /** 46 | * The query part of the URI for this request. If the full URI is {@code 47 | * http://foo.com/bar/baz?this=that}, then this method will return {@code this=that}. If there is 48 | * no query part, the returned {@code Optional} is empty. 49 | * 50 | * @return the query part of the URI, if any. 51 | */ 52 | Optional getQuery(); 53 | 54 | /** 55 | * The query parameters of this request. If the full URI is {@code 56 | * http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then the returned map will map {@code 57 | * thing} to the list {@code ["thing1", "thing2"]} and {@code cat} to the list with the single 58 | * element {@code "hat"}. 59 | * 60 | * @return a map where each key is the name of a query parameter and the corresponding {@code 61 | * List} value indicates every value that was associated with that name. 62 | */ 63 | Map> getQueryParameters(); 64 | 65 | /** 66 | * The first query parameter with the given name, if any. If the full URI is {@code 67 | * http://foo.com/bar?thing=thing1&thing=thing2&cat=hat}, then {@code 68 | * getFirstQueryParameter("thing")} will return {@code Optional.of("thing1")} and {@code 69 | * getFirstQueryParameter("something")} will return {@code Optional.empty()}. This is a more 70 | * convenient alternative to {@link #getQueryParameters}. 71 | * 72 | * @param name a query parameter name. 73 | * @return the first query parameter value with the given name, if any. 74 | */ 75 | default Optional getFirstQueryParameter(String name) { 76 | List parameters = getQueryParameters().get(name); 77 | if (parameters == null || parameters.isEmpty()) { 78 | return Optional.empty(); 79 | } 80 | return Optional.of(parameters.get(0)); 81 | } 82 | 83 | /** 84 | * Represents one part inside a multipart ({@code multipart/form-data}) HTTP request. Each such 85 | * part can have its own HTTP headers, which can be retrieved with the methods inherited from 86 | * {@link HttpMessage}. 87 | */ 88 | interface HttpPart extends HttpMessage { 89 | /** 90 | * Returns the filename associated with this part, if any. 91 | * 92 | * @return the filename associated with this part, if any. 93 | */ 94 | Optional getFileName(); 95 | } 96 | 97 | /** 98 | * Returns the parts inside this multipart ({@code multipart/form-data}) HTTP request. Each entry 99 | * in the returned map has the name of the part as its key and the contents as the associated 100 | * value. 101 | * 102 | * @return a map from part names to part contents. 103 | * @throws IllegalStateException if the {@link #getContentType() content type} is not {@code 104 | * multipart/form-data}. 105 | */ 106 | Map getParts(); 107 | } 108 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/HttpResponse.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | import java.io.BufferedWriter; 18 | import java.io.IOException; 19 | import java.io.OutputStream; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.Optional; 23 | 24 | /** 25 | * Represents the contents of an HTTP response that is being sent by a Cloud Function in response to 26 | * an HTTP request. 27 | */ 28 | public interface HttpResponse { 29 | /** 30 | * Sets the numeric HTTP status 32 | * code to use in the response. Most often this will be 200, which is the OK status. The named 33 | * constants in {@link java.net.HttpURLConnection}, such as {@link 34 | * java.net.HttpURLConnection#HTTP_OK HTTP_OK}, can be used as an alternative to writing numbers 35 | * in your source code. 36 | * 37 | * @param code the status code. 38 | */ 39 | void setStatusCode(int code); 40 | 41 | /** 42 | * Sets the numeric HTTP status 44 | * code and reason message to use in the response. For example
45 | * {@code setStatusCode(400, "Something went wrong")}. The named constants in {@link 46 | * java.net.HttpURLConnection}, such as {@link java.net.HttpURLConnection#HTTP_BAD_REQUEST 47 | * HTTP_BAD_REQUEST}, can be used as an alternative to writing numbers in your source code. 48 | * 49 | * @param code the status code. 50 | * @param message the status message. 51 | */ 52 | void setStatusCode(int code, String message); 53 | 54 | /** 55 | * Sets the value to use for the {@code Content-Type} header in the response. This may include a 56 | * character encoding, for example {@code setContentType("text/plain; charset=utf-8")}. 57 | * 58 | * @param contentType the content type. 59 | */ 60 | void setContentType(String contentType); 61 | 62 | /** 63 | * Returns the {@code Content-Type} that was previously set by {@link #setContentType}, or by 64 | * {@link #appendHeader} with a header name of {@code Content-Type}. If no {@code Content-Type} 65 | * has been set, returns {@code Optional.empty()}. 66 | * 67 | * @return the content type, if any. 68 | */ 69 | Optional getContentType(); 70 | 71 | /** 72 | * Includes the given header name with the given value in the response. This method may be called 73 | * several times for the same header, in which case the response will contain the header the same 74 | * number of times. 75 | * 76 | * @param header an HTTP header, such as {@code Content-Type}. 77 | * @param value a value to associate with that header. 78 | */ 79 | void appendHeader(String header, String value); 80 | 81 | /** 82 | * Returns the headers that have been defined for the response so far. This will contain at least 83 | * the headers that have been set via {@link #appendHeader} or {@link #setContentType}, and may 84 | * contain additional headers such as {@code Date}. 85 | * 86 | * @return a map where each key is a header name and the corresponding {@code List} value has one 87 | * entry for every value associated with that header. 88 | */ 89 | Map> getHeaders(); 90 | 91 | /** 92 | * Returns an {@link OutputStream} that can be used to write the body of the response. This method 93 | * is typically used to write binary data. If the body is text, the {@link #getWriter()} method is 94 | * more appropriate. 95 | * 96 | * @return the output stream. 97 | * @throws IOException if a valid {@link OutputStream} cannot be returned for some reason. 98 | * @throws IllegalStateException if {@link #getWriter} has already been called on this instance. 99 | */ 100 | OutputStream getOutputStream() throws IOException; 101 | 102 | /** 103 | * Returns a {@link BufferedWriter} that can be used to write the text body of the response. If 104 | * the written text will not be US-ASCII, you should specify a character encoding by calling 105 | * {@link #setContentType setContentType("text/foo; charset=bar")} or {@link #appendHeader 106 | * appendHeader("Content-Type", "text/foo; charset=bar")} before calling this method. 107 | * 108 | * @return the writer. 109 | * @throws IOException if a valid {@link BufferedWriter} cannot be returned for some reason. 110 | * @throws IllegalStateException if {@link #getOutputStream} has already been called on this 111 | * instance. 112 | */ 113 | BufferedWriter getWriter() throws IOException; 114 | } 115 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/RawBackgroundFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | /** 18 | * Represents a Cloud Function that is activated by an event. The payload of the event is a JSON 19 | * object, which can be parsed using a JSON package such as GSON. 21 | * 22 | *

Here is an example of an implementation that parses the JSON payload using Gson, to access its 23 | * {@code messageId} property: 24 | * 26 | * 27 | *

28 |  * public class Example implements RawBackgroundFunction {
29 |  *   private static final Logger logger = Logger.getLogger(Example.class.getName());
30 |  *
31 |  *  {@code @Override}
32 |  *   public void accept(String json, Context context) {
33 |  *     JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
34 |  *     JsonElement messageId = jsonObject.get("messageId");
35 |  *     String messageIdString = messageId.getAsJsonString();
36 |  *     logger.info("Got messageId " + messageIdString);
37 |  *   }
38 |  * }
39 |  * 
40 | * 41 | *

Here is an example of an implementation that deserializes the JSON payload into a Java object 42 | * for simpler access, again using Gson: 43 | * 44 | *

45 |  * public class Example implements RawBackgroundFunction {
46 |  *   private static final Logger logger = Logger.getLogger(Example.class.getName());
47 |  *
48 |  *  {@code @Override}
49 |  *   public void accept(String json, Context context) {
50 |  *     PubSubMessage message = new Gson().fromJson(json, PubSubMessage.class);
51 |  *     logger.info("Got messageId " + message.messageId);
52 |  *   }
53 |  * }
54 |  *
55 |  * // Where PubSubMessage is a user-defined class like this:
56 |  * public class PubSubMessage {
57 |  *   String data;
58 |  *  {@code Map} attributes;
59 |  *   String messageId;
60 |  *   String publishTime;
61 |  * }
62 |  * 
63 | */ 64 | @FunctionalInterface 65 | public interface RawBackgroundFunction { 66 | /** 67 | * Called to service an incoming event. This interface is implemented by user code to provide the 68 | * action for a given background function. If this method throws any exception (including any 69 | * {@link Error}) then the HTTP response will have a 500 status code. 70 | * 71 | * @param json the payload of the event, as a JSON string. 72 | * @param context the context of the event. This is a set of values that every event has, 73 | * separately from the payload, such as timestamp and event type. 74 | * @throws Exception to produce a 500 status code in the HTTP response. 75 | */ 76 | void accept(String json, Context context) throws Exception; 77 | } 78 | -------------------------------------------------------------------------------- /functions-framework-api/src/main/java/com/google/cloud/functions/TypedFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions; 16 | 17 | import java.lang.reflect.Type; 18 | 19 | /** 20 | * Represents a Cloud Function with a strongly typed interface that is activated by an HTTP request. 21 | */ 22 | @FunctionalInterface 23 | public interface TypedFunction { 24 | /** 25 | * Called to service an incoming HTTP request. This interface is implemented by user code to 26 | * provide the action for a given HTTP function. If this method throws any exception (including 27 | * any {@link Error}) then the HTTP response will have a 500 status code. 28 | * 29 | * @param arg the payload of the event, deserialized from the original JSON string. 30 | * @return invocation result or null to indicate the body of the response should be empty. 31 | * @throws Exception to produce a 500 status code in the HTTP response. 32 | */ 33 | public ResponseT apply(RequestT arg) throws Exception; 34 | 35 | /** 36 | * Called to get the the format object that handles request decoding and response encoding. If 37 | * null is returned a default JSON format is used. 38 | * 39 | * @return the {@link WireFormat} to use for serialization 40 | */ 41 | public default WireFormat getWireFormat() { 42 | return null; 43 | } 44 | 45 | /** 46 | * Describes how to deserialize request object and serialize response objects for an HTTP 47 | * invocation. 48 | */ 49 | public interface WireFormat { 50 | /** Serialize is expected to encode the object to the provided HttpResponse. */ 51 | void serialize(Object object, HttpResponse response) throws Exception; 52 | 53 | /** 54 | * Deserialize is expected to read an object of {@code Type} from the HttpRequest. The Type is 55 | * determined through reflection on the user's function. 56 | */ 57 | Object deserialize(HttpRequest request, Type type) throws Exception; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /invoker/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.4.1](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.4.0...java-function-invoker-v1.4.1) (2025-03-07) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * correct Cloud Event retry functionality ([#326](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/326)) ([9899a67](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/9899a67a9a8cb6ebb27a92cccb740e7e23d48578)) 9 | 10 | ## [1.4.0](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.3.3...java-function-invoker-v1.4.0) (2025-02-12) 11 | 12 | 13 | ### Features 14 | 15 | * Add execution id logging to uniquely identify request logs ([#319](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/319)) ([5ef5317](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/5ef53174b6cdbc644336121bc19bab6c4b90892d)) 16 | 17 | ## [1.3.3](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.3.2...java-function-invoker-v1.3.3) (2024-11-27) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * revert maven-source-plugin to 3.2.1 ([#303](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/303)) ([2db9a2b](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/2db9a2bec6ba93e7954e68c2301c5fc2fcc032d8)) 23 | 24 | ## [1.3.2](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.3.1...java-function-invoker-v1.3.2) (2024-09-18) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * avoid executing function when /favicon.ico or /robots.txt is called ([#226](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/226)) ([fca8676](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/fca867667db593699193da01b69a4cca7ca48fc8)) 30 | * server times out when specified by CLOUD_RUN_TIMEOUT_SECONDS ([#275](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/275)) ([9e91f57](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/9e91f57b12d73c655e3d7e226d21d54ccec32b73)) 31 | * set Thread Context ClassLoader correctly when invoking handler constructor ([#239](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/239)) ([9f7155b](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/9f7155b77574ec980ecf9e6dffbd2ee0398db8a7)) 32 | 33 | ## [1.3.1](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.3.0...java-function-invoker-v1.3.1) (2023-09-13) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * **functions:** include Implementation-Version key in invoker package manifest ([#221](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/221)) ([f3fe2ce](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/f3fe2ce46fcb1885137cdf504649612e7c31dc4c)) 39 | * typed declaration works correctly with http trigger ([#212](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/212)) ([b3045ad](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/b3045ad380cd23e37f5edec0d758031438bcb568)) 40 | 41 | ## [1.3.0](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.2.1...java-function-invoker-v1.3.0) (2023-06-01) 42 | 43 | 44 | ### Features 45 | 46 | * Define strongly typed function interface ([#186](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/186)) ([5264e35](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/5264e35b2522a789d65f0e0fd9bb5584694529eb)) 47 | 48 | 49 | ### Bug Fixes 50 | 51 | * bump org.eclipse.jetty dependency to 9.4.51 ([#201](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/201)) ([0102c8f](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/0102c8f543280ff5ba5727508f87083a9f54ef74)) 52 | 53 | ## [1.2.1](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.2.0...java-function-invoker-v1.2.1) (2023-03-02) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * retrieving http headers on request object should be case insenstive ([#178](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/178)) ([44da871](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/44da871e06e967ce132bea06c3b7c5d1b06ddd6b)) 59 | 60 | ## [1.2.0](https://github.com/GoogleCloudPlatform/functions-framework-java/compare/java-function-invoker-v1.1.1...java-function-invoker-v1.2.0) (2022-10-05) 61 | 62 | 63 | ### Features 64 | 65 | * allow to stop the invoker ([#128](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/128)) ([14908ca](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/14908caa9e5be824dfb74fff3a3234c4bce688e7)) 66 | * enable converting CloudEvent requests to Background Event requests ([#123](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/123)) ([1c4a014](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/1c4a01470cc4ee7b3de3c3d7ae4af24e47eb2810)) 67 | * Increase maximum concurrent requests for jetty server to 1000. ([#144](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/144)) ([439d0b5](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/439d0b5d77b2f765e65d84e7d5f31399e547d004)) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * Add build env vars support for function deployment. ([#133](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/133)) ([0e052f3](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/0e052f376231192278061ec79bcf9d710ec310f4)) 73 | * bump dependency versions ([#134](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/134)) ([faff79d](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/faff79d16c6df178d66f0185fb78fba003e60745)) 74 | * bump jetty version to 9.4.49.v20220914 ([#164](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/164)) ([f5231a2](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/f5231a2303aa3565b29d494936e40ee1ec78fdbb)) 75 | * make user function exceptions log level SEVERE ([#113](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/113)) ([1684c0e](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/1684c0ef55dc33f2c4c7f7514d99b0e7af75c44f)) 76 | * update conformance tests ([#108](https://github.com/GoogleCloudPlatform/functions-framework-java/issues/108)) ([72852d0](https://github.com/GoogleCloudPlatform/functions-framework-java/commit/72852d0f23cdaed48569245440dcd1533c8c7563)) 77 | -------------------------------------------------------------------------------- /invoker/conformance/buildpack_pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | com.google.cloud.functions.invoker 5 | conformance 6 | 0.0.0-SNAPSHOT 7 | 8 | GCF Conformance Tests 9 | 10 | A GCF project used to validate conformance to the Functions Framework contract 11 | using the Functions Framework Conformance tools. 12 | 13 | https://github.com/GoogleCloudPlatform/functions-framework-conformance 14 | 15 | 16 | UTF-8 17 | 11 18 | 11 19 | 20 | 21 | 22 | conformance 23 | file:./artifacts 24 | 25 | 26 | 27 | com.google.cloud.functions 28 | functions-framework-api 29 | FRAMEWORK-API-VERSION 30 | 31 | 32 | com.google.cloud.functions.invoker 33 | java-function-invoker 34 | INVOKER-VERSION 35 | 36 | 37 | com.google.code.gson 38 | gson 39 | 2.8.9 40 | 41 | 42 | io.cloudevents 43 | cloudevents-core 44 | 2.2.0 45 | 46 | 47 | io.cloudevents 48 | cloudevents-json-jackson 49 | 2.2.0 50 | 51 | 52 | -------------------------------------------------------------------------------- /invoker/conformance/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | java-function-invoker-parent 6 | com.google.cloud.functions.invoker 7 | 1.4.1 8 | 9 | 10 | com.google.cloud.functions.invoker 11 | conformance 12 | 1.4.1 13 | 14 | GCF Confromance Tests 15 | 16 | A GCF project used to validate conformance to the Functions Framework contract 17 | using the Functions Framework Conformance tools. 18 | 19 | https://github.com/GoogleCloudPlatform/functions-framework-conformance 20 | 21 | 22 | UTF-8 23 | 11 24 | 11 25 | 26 | 27 | 28 | 29 | com.google.cloud.functions 30 | functions-framework-api 31 | 1.1.4 32 | 33 | 34 | com.google.code.gson 35 | gson 36 | 2.12.1 37 | 38 | 39 | io.cloudevents 40 | cloudevents-core 41 | 4.0.1 42 | 43 | 44 | io.cloudevents 45 | cloudevents-json-jackson 46 | 4.0.1 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | com.google.cloud.functions 55 | function-maven-plugin 56 | 0.11.1 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /invoker/conformance/prerun.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is used for buildpack validation. 4 | # Its purpose is to run conformance tests using a buildpack 5 | # with the latest code of functions-framework. 6 | # 7 | # Note: buildpack_pom.xml contains the configuration to use the local versions 8 | # java-function-invoker and functions-framework-api. We will be using this file 9 | # for running our conformance tests with buildpack. 10 | # 11 | # Steps: 12 | # 13 | # - Clear out the temp directory 14 | # - Copy the conformance tests folder into temp 15 | # - Build java-function-invoker with version 0.0.0-SNAPSHOT into artifacts 16 | # folder 17 | # - Build functions-framework-api with version 0.0.0-SNAPSHOT into artifacts 18 | # folder 19 | # - Ensure that we use the buildpack_pom.xml file by renaming it to pom.xml 20 | 21 | set -e 22 | REPO_ROOT=$(git rev-parse --show-toplevel) 23 | 24 | rm -rf /tmp/tests 25 | mkdir /tmp/tests 26 | 27 | cp -r $REPO_ROOT/invoker/conformance /tmp/tests 28 | 29 | function get_mvn_version() { 30 | mvn -q \ 31 | -Dexec.executable=echo \ 32 | -Dexec.args='${project.version}' \ 33 | --non-recursive \ 34 | exec:exec 35 | } 36 | 37 | # Must first install a local version of the API package 38 | cd $REPO_ROOT/functions-framework-api 39 | mvn install -Dmaven.repo.local=/tmp/tests/conformance/artifacts 40 | FRAMEWORK_API_VERSION=$(get_mvn_version) 41 | 42 | # Build invoker packages against the latest API package 43 | cd $REPO_ROOT/invoker 44 | mvn install -Dmaven.repo.local=/tmp/tests/conformance/artifacts 45 | INVOKER_VERSION=$(get_mvn_version) 46 | 47 | rm /tmp/tests/conformance/pom.xml 48 | mv /tmp/tests/conformance/buildpack_pom.xml /tmp/tests/conformance/pom.xml 49 | 50 | sed -i "s/FRAMEWORK-API-VERSION/${FRAMEWORK_API_VERSION}/g" /tmp/tests/conformance/pom.xml 51 | sed -i "s/INVOKER-VERSION/${INVOKER_VERSION}/g" /tmp/tests/conformance/pom.xml 52 | 53 | cat /tmp/tests/conformance/pom.xml 54 | -------------------------------------------------------------------------------- /invoker/conformance/src/main/java/com/google/cloud/functions/conformance/BackgroundEventConformanceFunction.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.conformance; 2 | 3 | import com.google.cloud.functions.Context; 4 | import com.google.cloud.functions.RawBackgroundFunction; 5 | import com.google.gson.Gson; 6 | import com.google.gson.GsonBuilder; 7 | import com.google.gson.JsonElement; 8 | import com.google.gson.JsonObject; 9 | import java.io.BufferedWriter; 10 | import java.io.FileWriter; 11 | 12 | /** 13 | * This class is used by the Functions Framework Conformance Tools to validate the framework's 14 | * Background Event API. It can be run with the following command: 15 | * 16 | *
{@code
17 |  * $ functions-framework-conformance-client \
18 |  *   -cmd="mvn function:run -Drun.functionTarget=com.google.cloud.functions.conformance.BackgroundEventConformanceFunction" \
19 |  *   -type=legacyevent \
20 |  *   -buildpacks=false \
21 |  *   -validate-mapping=false \
22 |  *   -start-delay=10
23 |  * }
24 | */ 25 | public class BackgroundEventConformanceFunction implements RawBackgroundFunction { 26 | 27 | private static final Gson gson = new GsonBuilder().serializeNulls().setPrettyPrinting().create(); 28 | 29 | @Override 30 | public void accept(String data, Context context) throws Exception { 31 | try (BufferedWriter writer = new BufferedWriter(new FileWriter("function_output.json"))) { 32 | writer.write(serialize(data, context)); 33 | } 34 | } 35 | 36 | /** Create a structured JSON representation of the request context and data */ 37 | private String serialize(String data, Context context) { 38 | JsonObject contextJson = new JsonObject(); 39 | contextJson.addProperty("eventId", context.eventId()); 40 | contextJson.addProperty("timestamp", context.timestamp()); 41 | contextJson.addProperty("eventType", context.eventType()); 42 | 43 | if (context.resource().startsWith("{")) { 44 | JsonElement resource = gson.fromJson(context.resource(), JsonElement.class); 45 | contextJson.add("resource", resource); 46 | } else { 47 | contextJson.addProperty("resource", context.resource()); 48 | } 49 | 50 | JsonObject dataJson = gson.fromJson(data, JsonObject.class); 51 | 52 | JsonObject json = new JsonObject(); 53 | json.add("data", dataJson); 54 | json.add("context", contextJson); 55 | return gson.toJson(json); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /invoker/conformance/src/main/java/com/google/cloud/functions/conformance/CloudEventsConformanceFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.conformance; 16 | 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | 19 | import com.google.cloud.functions.CloudEventsFunction; 20 | import io.cloudevents.CloudEvent; 21 | import io.cloudevents.core.format.EventFormat; 22 | import io.cloudevents.core.provider.EventFormatProvider; 23 | import io.cloudevents.jackson.JsonFormat; 24 | import java.io.BufferedWriter; 25 | import java.io.FileWriter; 26 | 27 | /** 28 | * This class is used by the Functions Framework Conformance Tools to validate the framework's Cloud 29 | * Events API. It can be run with the following command: 30 | * 31 | *
{@code
32 |  * $ functions-framework-conformance-client \
33 |  *   -cmd="mvn function:run -Drun.functionTarget=com.google.cloud.functions.conformance.CloudEventsConformanceFunction" \
34 |  *   -type=cloudevent \
35 |  *   -buildpacks=false \
36 |  *   -validate-mapping=false \
37 |  *   -start-delay=5
38 |  * }
39 | */ 40 | public class CloudEventsConformanceFunction implements CloudEventsFunction { 41 | 42 | @Override 43 | public void accept(CloudEvent event) throws Exception { 44 | try (BufferedWriter writer = new BufferedWriter(new FileWriter("function_output.json"))) { 45 | EventFormat format = EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); 46 | writer.write(new String(format.serialize(event), UTF_8)); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /invoker/conformance/src/main/java/com/google/cloud/functions/conformance/ConcurrentHttpConformanceFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.conformance; 16 | 17 | import com.google.cloud.functions.HttpFunction; 18 | import com.google.cloud.functions.HttpRequest; 19 | import com.google.cloud.functions.HttpResponse; 20 | 21 | /** 22 | * This class is used by the Functions Framework Conformance Tools to validate concurrency for HTTP 23 | * functions. It can be run with the following command: 24 | * 25 | *
{@code
26 |  * $ functions-framework-conformance-client \
27 |  *   -cmd="mvn function:run -Drun.functionTarget=com.google.cloud.functions.conformance.ConcurrentHttpConformanceFunction" \
28 |  *   -type=http \
29 |  *   -buildpacks=false \
30 |  *   -validate-mapping=false \
31 |  *   -start-delay=5 \
32 |  *   -validate-concurrency=true
33 |  * }
34 | */ 35 | public class ConcurrentHttpConformanceFunction implements HttpFunction { 36 | 37 | @Override 38 | public void service(HttpRequest request, HttpResponse response) throws InterruptedException { 39 | Thread.sleep(1000); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /invoker/conformance/src/main/java/com/google/cloud/functions/conformance/HttpConformanceFunction.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.conformance; 16 | 17 | import com.google.cloud.functions.HttpFunction; 18 | import com.google.cloud.functions.HttpRequest; 19 | import com.google.cloud.functions.HttpResponse; 20 | import java.io.BufferedReader; 21 | import java.io.BufferedWriter; 22 | import java.io.FileWriter; 23 | import java.io.IOException; 24 | 25 | /** 26 | * This class is used by the Functions Framework Conformance Tools to validate the framework's HTTP 27 | * API. It can be run with the following command: 28 | * 29 | *
{@code
30 |  * $ functions-framework-conformance-client \
31 |  *   -cmd="mvn function:run -Drun.functionTarget=com.google.cloud.functions.conformance.HttpConformanceFunction" \
32 |  *   -type=http \
33 |  *   -buildpacks=false \
34 |  *   -validate-mapping=false \
35 |  *   -start-delay=5
36 |  * }
37 | */ 38 | public class HttpConformanceFunction implements HttpFunction { 39 | 40 | @Override 41 | public void service(HttpRequest request, HttpResponse response) throws IOException { 42 | try (BufferedReader reader = request.getReader(); 43 | BufferedWriter writer = new BufferedWriter(new FileWriter("function_output.json"))) { 44 | int c; 45 | while ((c = reader.read()) != -1) { 46 | writer.write(c); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /invoker/conformance/src/main/java/com/google/cloud/functions/conformance/TypedConformanceFunction.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.conformance; 2 | 3 | import com.google.cloud.functions.TypedFunction; 4 | import com.google.gson.annotations.SerializedName; 5 | 6 | public class TypedConformanceFunction 7 | implements TypedFunction { 8 | @Override 9 | public ConformanceResponse apply(ConformanceRequest req) throws Exception { 10 | return new ConformanceResponse(req); 11 | } 12 | } 13 | 14 | class ConformanceRequest { 15 | @SerializedName("message") 16 | public String message; 17 | } 18 | 19 | class ConformanceResponse { 20 | @SerializedName("payload") 21 | public ConformanceRequest payload = null; 22 | 23 | ConformanceResponse(ConformanceRequest payload) { 24 | this.payload = payload; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /invoker/core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | com.google.cloud.functions.invoker 6 | java-function-invoker-parent 7 | 1.4.1 8 | 9 | 10 | com.google.cloud.functions.invoker 11 | java-function-invoker 12 | 1.4.1 13 | GCF Java Invoker 14 | 15 | Application that invokes a GCF Java function. This application is a 16 | complete HTTP server that interprets incoming HTTP requests appropriately 17 | and forwards them to the function code. 18 | 19 | 20 | 21 | UTF-8 22 | 5.3.2 23 | 11 24 | 11 25 | 4.0.1 26 | 27 | 28 | 29 | 30 | Apache License, Version 2.0 31 | http://www.apache.org/licenses/LICENSE-2.0.txt 32 | repo 33 | 34 | 35 | 36 | 37 | scm:git:https://github.com/GoogleCloudPlatform/functions-framework-java.git 38 | scm:git:git@github.com:GoogleCloudPlatform/functions-framework-java.git 39 | https://github.com/GoogleCloudPlatform/functions-framework-java 40 | HEAD 41 | 42 | 43 | 44 | 45 | com.google.cloud.functions 46 | functions-framework-api 47 | 1.1.4 48 | 49 | 50 | javax.servlet 51 | javax.servlet-api 52 | 4.0.1 53 | 54 | 55 | io.cloudevents 56 | cloudevents-core 57 | ${cloudevents.sdk.version} 58 | 59 | 60 | io.cloudevents 61 | cloudevents-http-basic 62 | ${cloudevents.sdk.version} 63 | 64 | 65 | io.cloudevents 66 | cloudevents-json-jackson 67 | ${cloudevents.sdk.version} 68 | 69 | 70 | com.google.code.gson 71 | gson 72 | 2.12.1 73 | 74 | 75 | com.ryanharter.auto.value 76 | auto-value-gson 77 | 1.3.1 78 | provided 79 | 80 | 81 | com.ryanharter.auto.value 82 | auto-value-gson-annotations 83 | 0.8.0 84 | provided 85 | 86 | 87 | com.google.auto.value 88 | auto-value 89 | 1.11.0 90 | provided 91 | 92 | 93 | com.google.auto.value 94 | auto-value-annotations 95 | 1.11.0 96 | provided 97 | 98 | 99 | org.eclipse.jetty 100 | jetty-servlet 101 | 9.4.57.v20241219 102 | 103 | 104 | org.eclipse.jetty 105 | jetty-server 106 | 9.4.57.v20241219 107 | 108 | 109 | com.beust 110 | jcommander 111 | 1.82 112 | 113 | 114 | 115 | 116 | com.google.cloud.functions.invoker 117 | java-function-invoker-testfunction 118 | 1.4.1 119 | test-jar 120 | test 121 | 122 | 123 | org.mockito 124 | mockito-core 125 | 5.16.0 126 | test 127 | 128 | 129 | junit 130 | junit 131 | 4.13.2 132 | test 133 | 134 | 135 | com.google.re2j 136 | re2j 137 | 1.8 138 | 139 | 140 | com.google.truth 141 | truth 142 | 1.4.4 143 | test 144 | 145 | 146 | com.google.truth.extensions 147 | truth-java8-extension 148 | 1.4.4 149 | test 150 | 151 | 152 | org.eclipse.jetty 153 | jetty-client 154 | 9.4.57.v20241219 155 | test 156 | 157 | 158 | 159 | 160 | 161 | 162 | maven-jar-plugin 163 | 3.4.2 164 | 165 | 166 | 167 | com.google.cloud.functions.invoker.runner.Invoker 168 | true 169 | true 170 | 171 | 172 | 173 | 174 | 175 | org.apache.maven.plugins 176 | maven-shade-plugin 177 | 3.6.0 178 | 179 | 180 | package 181 | 182 | shade 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/CloudFunctionsContext.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.invoker; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.cloud.functions.Context; 19 | import com.google.gson.Gson; 20 | import com.google.gson.GsonBuilder; 21 | import com.google.gson.TypeAdapter; 22 | import java.lang.annotation.Retention; 23 | import java.lang.annotation.RetentionPolicy; 24 | import java.util.Collections; 25 | import java.util.Map; 26 | 27 | /** Event context (metadata) for events handled by Cloud Functions. */ 28 | @AutoValue 29 | abstract class CloudFunctionsContext implements Context { 30 | // AutoValue recognizes any annotation called @Nullable, so no need to import this from anywhere. 31 | @Retention(RetentionPolicy.SOURCE) 32 | @interface Nullable {} 33 | 34 | @Override 35 | @Nullable 36 | public abstract String eventId(); 37 | 38 | @Override 39 | @Nullable 40 | public abstract String timestamp(); 41 | 42 | @Override 43 | @Nullable 44 | public abstract String eventType(); 45 | 46 | @Override 47 | @Nullable 48 | public abstract String resource(); 49 | 50 | // TODO: expose this in the Context interface (as a default method). 51 | abstract Map params(); 52 | 53 | @Nullable 54 | abstract String domain(); 55 | 56 | @Override 57 | public abstract Map attributes(); 58 | 59 | public static TypeAdapter typeAdapter(Gson gson) { 60 | return new AutoValue_CloudFunctionsContext.GsonTypeAdapter(gson); 61 | } 62 | 63 | static Builder builder() { 64 | return new AutoValue_CloudFunctionsContext.Builder() 65 | .setParams(Collections.emptyMap()) 66 | .setAttributes(Collections.emptyMap()); 67 | } 68 | 69 | @AutoValue.Builder 70 | abstract static class Builder { 71 | abstract Builder setEventId(String x); 72 | 73 | abstract Builder setTimestamp(String x); 74 | 75 | abstract Builder setEventType(String x); 76 | 77 | abstract Builder setResource(String x); 78 | 79 | abstract Builder setParams(Map x); 80 | 81 | abstract Builder setAttributes(Map value); 82 | 83 | abstract Builder setDomain(String x); 84 | 85 | abstract CloudFunctionsContext build(); 86 | } 87 | 88 | /** 89 | * Depending on the event type, the {@link Context#resource()} field is either a JSON string 90 | * (complete with encosing quotes) or a JSON object. This class allows us to redeserialize that 91 | * JSON representation into its components. 92 | */ 93 | @AutoValue 94 | abstract static class Resource { 95 | abstract @Nullable String service(); 96 | 97 | abstract String name(); 98 | 99 | abstract @Nullable String type(); 100 | 101 | static TypeAdapter typeAdapter(Gson gson) { 102 | return new AutoValue_CloudFunctionsContext_Resource.GsonTypeAdapter(gson); 103 | } 104 | 105 | static Resource from(String s) { 106 | if (s.startsWith("{") && (s.endsWith("}") || s.endsWith("}\n"))) { 107 | TypeAdapter typeAdapter = typeAdapter(new Gson()); 108 | Gson gson = new GsonBuilder().registerTypeAdapter(Resource.class, typeAdapter).create(); 109 | return gson.fromJson(s, Resource.class); 110 | } 111 | return builder().setName(s).build(); 112 | } 113 | 114 | static Builder builder() { 115 | return new AutoValue_CloudFunctionsContext_Resource.Builder(); 116 | } 117 | 118 | @AutoValue.Builder 119 | abstract static class Builder { 120 | abstract Builder setService(String x); 121 | 122 | abstract Builder setName(String x); 123 | 124 | abstract Builder setType(String x); 125 | 126 | abstract Resource build(); 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/Event.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.invoker; 16 | 17 | import com.google.auto.value.AutoValue; 18 | import com.google.gson.JsonDeserializationContext; 19 | import com.google.gson.JsonDeserializer; 20 | import com.google.gson.JsonElement; 21 | import com.google.gson.JsonObject; 22 | import com.google.gson.JsonParseException; 23 | import java.lang.reflect.Type; 24 | import java.time.OffsetDateTime; 25 | import java.time.format.DateTimeFormatter; 26 | 27 | /** 28 | * Represents an event that should be handled by a background function. This is an internal format 29 | * which is later converted to actual background function parameter types. 30 | */ 31 | @AutoValue 32 | abstract class Event { 33 | static Event of(JsonElement data, CloudFunctionsContext context) { 34 | return new AutoValue_Event(data, context); 35 | } 36 | 37 | abstract JsonElement getData(); 38 | 39 | abstract CloudFunctionsContext getContext(); 40 | 41 | /** Custom deserializer that supports both GCF beta and GCF GA event formats. */ 42 | static class EventDeserializer implements JsonDeserializer { 43 | 44 | @Override 45 | public Event deserialize( 46 | JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) 47 | throws JsonParseException { 48 | JsonObject root = jsonElement.getAsJsonObject(); 49 | 50 | JsonElement data = root.get("data"); 51 | CloudFunctionsContext context; 52 | 53 | if (root.has("context")) { 54 | JsonObject contextCopy = root.getAsJsonObject("context").deepCopy(); 55 | context = 56 | jsonDeserializationContext.deserialize( 57 | adjustContextResource(contextCopy), CloudFunctionsContext.class); 58 | } else if (isPubSubEmulatorPayload(root)) { 59 | JsonObject message = root.getAsJsonObject("message"); 60 | 61 | String timestampString = 62 | message.has("publishTime") 63 | ? message.get("publishTime").getAsString() 64 | : DateTimeFormatter.ISO_INSTANT.format(OffsetDateTime.now()); 65 | 66 | context = 67 | CloudFunctionsContext.builder() 68 | .setEventType("google.pubsub.topic.publish") 69 | .setTimestamp(timestampString) 70 | .setEventId(message.get("messageId").getAsString()) 71 | .setResource( 72 | "{" 73 | + "\"name\":null," 74 | + "\"service\":\"pubsub.googleapis.com\"," 75 | + "\"type\":\"type.googleapis.com/google.pubsub.v1.PubsubMessage\"" 76 | + "}") 77 | .build(); 78 | 79 | JsonObject marshalledData = new JsonObject(); 80 | marshalledData.addProperty("@type", "type.googleapis.com/google.pubsub.v1.PubsubMessage"); 81 | marshalledData.add("data", message.get("data")); 82 | if (message.has("attributes")) { 83 | marshalledData.add("attributes", message.get("attributes")); 84 | } 85 | data = marshalledData; 86 | } else { 87 | JsonObject rootCopy = root.deepCopy(); 88 | rootCopy.remove("data"); 89 | context = 90 | jsonDeserializationContext.deserialize( 91 | adjustContextResource(rootCopy), CloudFunctionsContext.class); 92 | } 93 | return Event.of(data, context); 94 | } 95 | 96 | private boolean isPubSubEmulatorPayload(JsonObject root) { 97 | if (root.has("subscription") && root.has("message") && root.get("message").isJsonObject()) { 98 | JsonObject message = root.getAsJsonObject("message"); 99 | return message.has("data") && message.has("messageId"); 100 | } 101 | return false; 102 | } 103 | 104 | /** 105 | * Replaces 'resource' member from context JSON with its string equivalent. The original 106 | * 'resource' member can be a JSON object itself while {@link CloudFunctionsContext} requires it 107 | * to be a string. 108 | */ 109 | private JsonObject adjustContextResource(JsonObject contextObject) { 110 | if (contextObject.has("resource")) { 111 | JsonElement resourceElement = contextObject.get("resource"); 112 | if (resourceElement.isJsonObject()) { 113 | contextObject.addProperty("resource", resourceElement.toString()); 114 | } 115 | } 116 | return contextObject; 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/HttpFunctionExecutor.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.invoker; 16 | 17 | import com.google.cloud.functions.HttpFunction; 18 | import com.google.cloud.functions.invoker.gcf.ExecutionIdUtil; 19 | import com.google.cloud.functions.invoker.http.HttpRequestImpl; 20 | import com.google.cloud.functions.invoker.http.HttpResponseImpl; 21 | import java.util.logging.Level; 22 | import java.util.logging.Logger; 23 | import javax.servlet.http.HttpServlet; 24 | import javax.servlet.http.HttpServletRequest; 25 | import javax.servlet.http.HttpServletResponse; 26 | 27 | /** Executes the user's method. */ 28 | public class HttpFunctionExecutor extends HttpServlet { 29 | private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker"); 30 | 31 | private final HttpFunction function; 32 | private final ExecutionIdUtil executionIdUtil = new ExecutionIdUtil(); 33 | 34 | private HttpFunctionExecutor(HttpFunction function) { 35 | this.function = function; 36 | } 37 | 38 | /** 39 | * Makes a {@link HttpFunctionExecutor} for the given class. 40 | * 41 | * @throws RuntimeException if either the given class does not implement {@link HttpFunction} or 42 | * we are unable to construct an instance using its no-arg constructor. 43 | */ 44 | public static HttpFunctionExecutor forClass(Class functionClass) { 45 | if (!HttpFunction.class.isAssignableFrom(functionClass)) { 46 | throw new RuntimeException( 47 | "Class " 48 | + functionClass.getName() 49 | + " does not implement " 50 | + HttpFunction.class.getName()); 51 | } 52 | Class httpFunctionClass = functionClass.asSubclass(HttpFunction.class); 53 | ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader(); 54 | try { 55 | Thread.currentThread().setContextClassLoader(httpFunctionClass.getClassLoader()); 56 | HttpFunction httpFunction = httpFunctionClass.getConstructor().newInstance(); 57 | return new HttpFunctionExecutor(httpFunction); 58 | } catch (ReflectiveOperationException e) { 59 | throw new RuntimeException( 60 | "Could not construct an instance of " + functionClass.getName() + ": " + e, e); 61 | } finally { 62 | Thread.currentThread().setContextClassLoader(oldContextLoader); 63 | } 64 | } 65 | 66 | /** Executes the user's method, can handle all HTTP type methods. */ 67 | @Override 68 | public void service(HttpServletRequest req, HttpServletResponse res) { 69 | HttpRequestImpl reqImpl = new HttpRequestImpl(req); 70 | HttpResponseImpl respImpl = new HttpResponseImpl(res); 71 | ClassLoader oldContextLoader = Thread.currentThread().getContextClassLoader(); 72 | try { 73 | executionIdUtil.storeExecutionId(req); 74 | Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader()); 75 | function.service(reqImpl, respImpl); 76 | } catch (Throwable t) { 77 | logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t); 78 | res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 79 | } finally { 80 | Thread.currentThread().setContextClassLoader(oldContextLoader); 81 | executionIdUtil.removeExecutionId(); 82 | respImpl.flush(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/TypedFunctionExecutor.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker; 2 | 3 | import com.google.cloud.functions.HttpRequest; 4 | import com.google.cloud.functions.HttpResponse; 5 | import com.google.cloud.functions.TypedFunction; 6 | import com.google.cloud.functions.TypedFunction.WireFormat; 7 | import com.google.cloud.functions.invoker.http.HttpRequestImpl; 8 | import com.google.cloud.functions.invoker.http.HttpResponseImpl; 9 | import com.google.gson.Gson; 10 | import com.google.gson.GsonBuilder; 11 | import java.io.BufferedReader; 12 | import java.io.BufferedWriter; 13 | import java.lang.reflect.Type; 14 | import java.util.Arrays; 15 | import java.util.Optional; 16 | import java.util.logging.Level; 17 | import java.util.logging.Logger; 18 | import javax.servlet.http.HttpServlet; 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.servlet.http.HttpServletResponse; 21 | 22 | public class TypedFunctionExecutor extends HttpServlet { 23 | private static final String APPLY_METHOD = "apply"; 24 | private static final Logger logger = Logger.getLogger("com.google.cloud.functions.invoker"); 25 | 26 | private final Type argType; 27 | private final TypedFunction function; 28 | private final WireFormat format; 29 | 30 | private TypedFunctionExecutor( 31 | Type argType, TypedFunction func, WireFormat format) { 32 | this.argType = argType; 33 | this.function = func; 34 | this.format = format; 35 | } 36 | 37 | public static TypedFunctionExecutor forClass(Class functionClass) { 38 | if (!TypedFunction.class.isAssignableFrom(functionClass)) { 39 | throw new RuntimeException( 40 | "Class " 41 | + functionClass.getName() 42 | + " does not implement " 43 | + TypedFunction.class.getName()); 44 | } 45 | @SuppressWarnings("unchecked") 46 | Class> typedFunctionClass = 47 | (Class>) functionClass.asSubclass(TypedFunction.class); 48 | 49 | Optional argType = handlerTypeArgument(typedFunctionClass); 50 | if (argType.isEmpty()) { 51 | throw new RuntimeException( 52 | "Class " 53 | + typedFunctionClass.getName() 54 | + " does not implement " 55 | + TypedFunction.class.getName()); 56 | } 57 | 58 | TypedFunction typedFunction; 59 | try { 60 | typedFunction = typedFunctionClass.getDeclaredConstructor().newInstance(); 61 | } catch (Exception e) { 62 | throw new RuntimeException( 63 | "Class " 64 | + typedFunctionClass.getName() 65 | + " must declare a valid default constructor to be usable as a strongly typed" 66 | + " function. Could not use constructor: " 67 | + e.toString()); 68 | } 69 | 70 | WireFormat format = typedFunction.getWireFormat(); 71 | if (format == null) { 72 | format = LazyDefaultFormatHolder.defaultFormat; 73 | } 74 | 75 | @SuppressWarnings("unchecked") 76 | TypedFunctionExecutor executor = 77 | new TypedFunctionExecutor( 78 | argType.orElseThrow(), (TypedFunction) typedFunction, format); 79 | return executor; 80 | } 81 | 82 | /** 83 | * Returns the {@code ReqT} of a concrete class that implements {@link TypedFunction 84 | * TypedFunction}. Returns an empty {@link Optional} if {@code ReqT} can't be 85 | * determined. 86 | */ 87 | static Optional handlerTypeArgument(Class> functionClass) { 88 | return Arrays.stream(functionClass.getMethods()) 89 | .filter(method -> method.getName().equals(APPLY_METHOD) && method.getParameterCount() == 1) 90 | .map(method -> method.getGenericParameterTypes()[0]) 91 | .filter(type -> type != Object.class) 92 | .findFirst(); 93 | } 94 | 95 | /** Executes the user's method, can handle all HTTP type methods. */ 96 | @Override 97 | public void service(HttpServletRequest req, HttpServletResponse res) { 98 | HttpRequestImpl reqImpl = new HttpRequestImpl(req); 99 | HttpResponseImpl resImpl = new HttpResponseImpl(res); 100 | ClassLoader oldContextClassLoader = Thread.currentThread().getContextClassLoader(); 101 | 102 | try { 103 | Thread.currentThread().setContextClassLoader(function.getClass().getClassLoader()); 104 | handleRequest(reqImpl, resImpl); 105 | } finally { 106 | Thread.currentThread().setContextClassLoader(oldContextClassLoader); 107 | resImpl.flush(); 108 | } 109 | } 110 | 111 | private void handleRequest(HttpRequest req, HttpResponse res) { 112 | Object reqObj; 113 | try { 114 | reqObj = format.deserialize(req, argType); 115 | } catch (Throwable t) { 116 | logger.log(Level.SEVERE, "Failed to parse request for " + function.getClass().getName(), t); 117 | res.setStatusCode(HttpServletResponse.SC_BAD_REQUEST); 118 | return; 119 | } 120 | 121 | Object resObj; 122 | try { 123 | resObj = function.apply(reqObj); 124 | } catch (Throwable t) { 125 | logger.log(Level.SEVERE, "Failed to execute " + function.getClass().getName(), t); 126 | res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 127 | return; 128 | } 129 | 130 | try { 131 | format.serialize(resObj, res); 132 | } catch (Throwable t) { 133 | logger.log( 134 | Level.SEVERE, "Failed to serialize response for " + function.getClass().getName(), t); 135 | res.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); 136 | return; 137 | } 138 | } 139 | 140 | private static class LazyDefaultFormatHolder { 141 | static final WireFormat defaultFormat = new GsonWireFormat(); 142 | } 143 | 144 | private static class GsonWireFormat implements TypedFunction.WireFormat { 145 | private final Gson gson = new GsonBuilder().create(); 146 | 147 | @Override 148 | public void serialize(Object object, HttpResponse response) throws Exception { 149 | if (object == null) { 150 | response.setStatusCode(HttpServletResponse.SC_NO_CONTENT); 151 | return; 152 | } 153 | try (BufferedWriter bodyWriter = response.getWriter()) { 154 | gson.toJson(object, bodyWriter); 155 | } 156 | } 157 | 158 | @Override 159 | public Object deserialize(HttpRequest request, Type type) throws Exception { 160 | try (BufferedReader bodyReader = request.getReader()) { 161 | return gson.fromJson(bodyReader, type); 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/ExecutionIdUtil.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.gcf; 2 | 3 | import java.util.Base64; 4 | import java.util.Random; 5 | import java.util.concurrent.ThreadLocalRandom; 6 | import java.util.logging.Handler; 7 | import java.util.logging.Logger; 8 | import javax.servlet.http.HttpServletRequest; 9 | 10 | /** 11 | * A helper class that either fetches a unique execution id from request HTTP headers or generates a 12 | * random id. 13 | */ 14 | public final class ExecutionIdUtil { 15 | private static final Logger rootLogger = Logger.getLogger(""); 16 | private static final int EXECUTION_ID_LENGTH = 12; 17 | private static final String EXECUTION_ID_HTTP_HEADER = "HTTP_FUNCTION_EXECUTION_ID"; 18 | private static final String LOG_EXECUTION_ID_ENV_NAME = "LOG_EXECUTION_ID"; 19 | 20 | private final Random random = ThreadLocalRandom.current(); 21 | 22 | /** 23 | * Add mapping to root logger from current thread id to execution id. This mapping will be used to 24 | * append the execution id to log lines. 25 | */ 26 | public void storeExecutionId(HttpServletRequest request) { 27 | if (!executionIdLoggingEnabled()) { 28 | return; 29 | } 30 | for (Handler handler : rootLogger.getHandlers()) { 31 | if (handler instanceof JsonLogHandler) { 32 | String id = getOrGenerateExecutionId(request); 33 | ((JsonLogHandler) handler).addExecutionId(Thread.currentThread().getId(), id); 34 | } 35 | } 36 | } 37 | 38 | /** Remove mapping from curent thread to request execution id */ 39 | public void removeExecutionId() { 40 | if (!executionIdLoggingEnabled()) { 41 | return; 42 | } 43 | for (Handler handler : rootLogger.getHandlers()) { 44 | if (handler instanceof JsonLogHandler) { 45 | ((JsonLogHandler) handler).removeExecutionId(Thread.currentThread().getId()); 46 | } 47 | } 48 | } 49 | 50 | private String getOrGenerateExecutionId(HttpServletRequest request) { 51 | String executionId = request.getHeader(EXECUTION_ID_HTTP_HEADER); 52 | if (executionId == null) { 53 | byte[] array = new byte[EXECUTION_ID_LENGTH]; 54 | random.nextBytes(array); 55 | executionId = Base64.getEncoder().encodeToString(array); 56 | } 57 | return executionId; 58 | } 59 | 60 | private boolean executionIdLoggingEnabled() { 61 | return Boolean.parseBoolean(System.getenv().getOrDefault(LOG_EXECUTION_ID_ENV_NAME, "false")); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/gcf/JsonLogHandler.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.gcf; 2 | 3 | import java.io.PrintStream; 4 | import java.io.PrintWriter; 5 | import java.io.StringWriter; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.ConcurrentMap; 10 | import java.util.logging.Handler; 11 | import java.util.logging.Level; 12 | import java.util.logging.LogRecord; 13 | 14 | /** 15 | * A log handler that publishes log messages in a json format. This is StackDriver's "structured logging" format. 17 | */ 18 | public final class JsonLogHandler extends Handler { 19 | private static final String SOURCE_LOCATION_KEY = "\"logging.googleapis.com/sourceLocation\": "; 20 | private static final String LOG_EXECUTION_ID_ENV_NAME = "LOG_EXECUTION_ID"; 21 | 22 | private static final String DEBUG = "DEBUG"; 23 | private static final String INFO = "INFO"; 24 | private static final String WARNING = "WARNING"; 25 | private static final String ERROR = "ERROR"; 26 | private static final String DEFAULT = "DEFAULT"; 27 | 28 | private final PrintStream out; 29 | private final boolean closePrintStreamOnClose; 30 | // This map is used to track execution id for currently running Jetty requests. Mapping thread 31 | // id to request works because of an implementation detail of Jetty thread pool handling. 32 | // Jetty worker threads completely handle a request before beginning work on a new request. 33 | // NOTE: Store thread id as a string to avoid comparison failures between int and long. 34 | // 35 | // Jetty Documentation (https://jetty.org/docs/jetty/10/programming-guide/arch/threads.html) 36 | private static final ConcurrentMap executionIdByThreadMap = 37 | new ConcurrentHashMap<>(); 38 | 39 | public JsonLogHandler(PrintStream out, boolean closePrintStreamOnClose) { 40 | this.out = out; 41 | this.closePrintStreamOnClose = closePrintStreamOnClose; 42 | } 43 | 44 | @Override 45 | public void publish(LogRecord record) { 46 | // We avoid String.format and String.join even though they would simplify the code. 47 | // Logging code often shows up in profiling so we want to make this fast and StringBuilder is 48 | // more performant. 49 | StringBuilder json = new StringBuilder("{"); 50 | appendSeverity(json, record); 51 | appendSourceLocation(json, record); 52 | appendExecutionId(json, record); 53 | appendMessage(json, record); // must be last, see appendMessage 54 | json.append("}"); 55 | // We must output the log all at once (should only call println once per call to publish) 56 | out.println(json); 57 | } 58 | 59 | private static void appendMessage(StringBuilder json, LogRecord record) { 60 | // This must be the last item in the JSON object, because it has no trailing comma. JSON is 61 | // unforgiving about commas and you can't have one just before }. 62 | json.append("\"message\": \"").append(escapeString(record.getMessage())); 63 | if (record.getThrown() != null) { 64 | json.append("\\n").append(escapeString(getStackTraceAsString(record.getThrown()))); 65 | } 66 | json.append("\""); 67 | } 68 | 69 | private static void appendSeverity(StringBuilder json, LogRecord record) { 70 | json.append("\"severity\": \"").append(levelToSeverity(record.getLevel())).append("\", "); 71 | } 72 | 73 | private static String levelToSeverity(Level level) { 74 | int intLevel = (level == null) ? 0 : level.intValue(); 75 | switch (intLevel) { 76 | case 300: // FINEST 77 | case 400: // FINER 78 | case 500: // FINE 79 | return DEBUG; 80 | case 700: // CONFIG 81 | case 800: // INFO 82 | // Java's CONFIG is lower than its INFO, while Stackdriver's NOTICE is greater than its 83 | // INFO. So despite the similarity, we don't try to use NOTICE for CONFIG. 84 | return INFO; 85 | case 900: // WARNING 86 | return WARNING; 87 | case 1000: // SEVERE 88 | return ERROR; 89 | default: 90 | return DEFAULT; 91 | } 92 | } 93 | 94 | private static void appendSourceLocation(StringBuilder json, LogRecord record) { 95 | if (record.getSourceClassName() == null && record.getSourceMethodName() == null) { 96 | return; 97 | } 98 | List entries = new ArrayList<>(); 99 | if (record.getSourceClassName() != null) { 100 | // TODO: Handle nested classes. If the source class name is com.example.Foo$Bar then the 101 | // source file is com/example/Foo.java, not com/example/Foo$Bar.java. 102 | String fileName = record.getSourceClassName().replace('.', '/') + ".java"; 103 | entries.add("\"file\": \"" + escapeString(fileName) + "\""); 104 | } 105 | if (record.getSourceMethodName() != null) { 106 | entries.add("\"method\": \"" + escapeString(record.getSourceMethodName()) + "\""); 107 | } 108 | json.append(SOURCE_LOCATION_KEY).append("{").append(String.join(", ", entries)).append("}, "); 109 | } 110 | 111 | private void appendExecutionId(StringBuilder json, LogRecord record) { 112 | if (executionIdLoggingEnabled()) { 113 | json.append("\"execution_id\": \"") 114 | .append(executionIdByThreadMap.get(Integer.toString(record.getThreadID()))) 115 | .append("\", "); 116 | } 117 | } 118 | 119 | private static String escapeString(String s) { 120 | return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); 121 | } 122 | 123 | private static String getStackTraceAsString(Throwable t) { 124 | StringWriter stringWriter = new StringWriter(); 125 | t.printStackTrace(new PrintWriter(stringWriter)); 126 | return stringWriter.toString(); 127 | } 128 | 129 | @Override 130 | public void flush() { 131 | out.flush(); 132 | } 133 | 134 | @Override 135 | public void close() throws SecurityException { 136 | if (closePrintStreamOnClose) { 137 | out.close(); 138 | } 139 | } 140 | 141 | public void addExecutionId(long threadId, String executionId) { 142 | executionIdByThreadMap.put(Long.toString(threadId), executionId); 143 | } 144 | 145 | public void removeExecutionId(long threadId) { 146 | executionIdByThreadMap.remove(Long.toString(threadId)); 147 | } 148 | 149 | private boolean executionIdLoggingEnabled() { 150 | return Boolean.parseBoolean(System.getenv().getOrDefault(LOG_EXECUTION_ID_ENV_NAME, "false")); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpRequestImpl.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.invoker.http; 16 | 17 | import static java.util.stream.Collectors.toMap; 18 | 19 | import com.google.cloud.functions.HttpRequest; 20 | import java.io.BufferedReader; 21 | import java.io.IOException; 22 | import java.io.InputStream; 23 | import java.io.InputStreamReader; 24 | import java.io.UncheckedIOException; 25 | import java.util.AbstractMap.SimpleEntry; 26 | import java.util.ArrayList; 27 | import java.util.Arrays; 28 | import java.util.Collection; 29 | import java.util.Collections; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.Optional; 33 | import java.util.TreeMap; 34 | import java.util.regex.Matcher; 35 | import java.util.regex.Pattern; 36 | import javax.servlet.ServletException; 37 | import javax.servlet.http.HttpServletRequest; 38 | import javax.servlet.http.Part; 39 | 40 | public class HttpRequestImpl implements HttpRequest { 41 | private final HttpServletRequest request; 42 | 43 | public HttpRequestImpl(HttpServletRequest request) { 44 | this.request = request; 45 | } 46 | 47 | @Override 48 | public String getMethod() { 49 | return request.getMethod(); 50 | } 51 | 52 | @Override 53 | public String getUri() { 54 | String url = request.getRequestURL().toString(); 55 | if (request.getQueryString() != null) { 56 | url += "?" + request.getQueryString(); 57 | } 58 | return url; 59 | } 60 | 61 | @Override 62 | public String getPath() { 63 | return request.getRequestURI(); 64 | } 65 | 66 | @Override 67 | public Optional getQuery() { 68 | return Optional.ofNullable(request.getQueryString()); 69 | } 70 | 71 | @Override 72 | public Map> getQueryParameters() { 73 | return request.getParameterMap().entrySet().stream() 74 | .collect(toMap(Map.Entry::getKey, e -> Arrays.asList(e.getValue()))); 75 | } 76 | 77 | @Override 78 | public Map getParts() { 79 | String contentType = request.getContentType(); 80 | if (contentType == null || !request.getContentType().startsWith("multipart/form-data")) { 81 | throw new IllegalStateException("Content-Type must be multipart/form-data: " + contentType); 82 | } 83 | try { 84 | return request.getParts().stream().collect(toMap(Part::getName, HttpPartImpl::new)); 85 | } catch (IOException e) { 86 | throw new UncheckedIOException(e); 87 | } catch (ServletException e) { 88 | throw new RuntimeException(e.getMessage(), e); 89 | } 90 | } 91 | 92 | @Override 93 | public Optional getContentType() { 94 | return Optional.ofNullable(request.getContentType()); 95 | } 96 | 97 | @Override 98 | public long getContentLength() { 99 | return request.getContentLength(); 100 | } 101 | 102 | @Override 103 | public Optional getCharacterEncoding() { 104 | return Optional.ofNullable(request.getCharacterEncoding()); 105 | } 106 | 107 | @Override 108 | public InputStream getInputStream() throws IOException { 109 | return request.getInputStream(); 110 | } 111 | 112 | @Override 113 | public BufferedReader getReader() throws IOException { 114 | return request.getReader(); 115 | } 116 | 117 | @Override 118 | public Map> getHeaders() { 119 | return Collections.list(request.getHeaderNames()).stream() 120 | .collect( 121 | toMap( 122 | name -> name, 123 | name -> Collections.list(request.getHeaders(name)), 124 | (a, b) -> b, 125 | () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); 126 | } 127 | 128 | private static class HttpPartImpl implements HttpPart { 129 | private final Part part; 130 | 131 | private HttpPartImpl(Part part) { 132 | this.part = part; 133 | } 134 | 135 | @Override 136 | public Optional getFileName() { 137 | return Optional.ofNullable(part.getSubmittedFileName()); 138 | } 139 | 140 | @Override 141 | public Optional getContentType() { 142 | return Optional.ofNullable(part.getContentType()); 143 | } 144 | 145 | @Override 146 | public long getContentLength() { 147 | return part.getSize(); 148 | } 149 | 150 | @Override 151 | public Optional getCharacterEncoding() { 152 | String contentType = getContentType().orElse(null); 153 | if (contentType == null) { 154 | return Optional.empty(); 155 | } 156 | Pattern charsetPattern = Pattern.compile("(?i).*;\\s*charset\\s*=([^;\\s]*)\\s*(;|$)"); 157 | Matcher matcher = charsetPattern.matcher(contentType); 158 | return matcher.matches() ? Optional.of(matcher.group(1)) : Optional.empty(); 159 | } 160 | 161 | @Override 162 | public InputStream getInputStream() throws IOException { 163 | return part.getInputStream(); 164 | } 165 | 166 | @Override 167 | public BufferedReader getReader() throws IOException { 168 | String encoding = getCharacterEncoding().orElse("utf-8"); 169 | return new BufferedReader(new InputStreamReader(getInputStream(), encoding)); 170 | } 171 | 172 | @Override 173 | public Map> getHeaders() { 174 | return part.getHeaderNames().stream() 175 | .map(name -> new SimpleEntry<>(name, list(part.getHeaders(name)))) 176 | .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 177 | } 178 | 179 | private static List list(Collection collection) { 180 | return (collection instanceof List) ? (List) collection : new ArrayList<>(collection); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/http/HttpResponseImpl.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.invoker.http; 16 | 17 | import static java.util.stream.Collectors.toMap; 18 | 19 | import com.google.cloud.functions.HttpResponse; 20 | import java.io.BufferedWriter; 21 | import java.io.IOException; 22 | import java.io.OutputStream; 23 | import java.util.ArrayList; 24 | import java.util.Collection; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.Optional; 28 | import java.util.TreeMap; 29 | import javax.servlet.http.HttpServletResponse; 30 | 31 | public class HttpResponseImpl implements HttpResponse { 32 | private final HttpServletResponse response; 33 | 34 | public HttpResponseImpl(HttpServletResponse response) { 35 | this.response = response; 36 | } 37 | 38 | @Override 39 | public void setStatusCode(int code) { 40 | response.setStatus(code); 41 | } 42 | 43 | @Override 44 | @SuppressWarnings("deprecation") 45 | public void setStatusCode(int code, String message) { 46 | response.setStatus(code, message); 47 | } 48 | 49 | @Override 50 | public void setContentType(String contentType) { 51 | response.setContentType(contentType); 52 | } 53 | 54 | @Override 55 | public Optional getContentType() { 56 | return Optional.ofNullable(response.getContentType()); 57 | } 58 | 59 | @Override 60 | public void appendHeader(String key, String value) { 61 | response.addHeader(key, value); 62 | } 63 | 64 | @Override 65 | public Map> getHeaders() { 66 | return response.getHeaderNames().stream() 67 | .collect( 68 | toMap( 69 | name -> name, 70 | name -> new ArrayList<>(response.getHeaders(name)), 71 | (a, b) -> b, 72 | () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER))); 73 | } 74 | 75 | private static List list(Collection collection) { 76 | return (collection instanceof List) ? (List) collection : new ArrayList<>(collection); 77 | } 78 | 79 | @Override 80 | public OutputStream getOutputStream() throws IOException { 81 | return response.getOutputStream(); 82 | } 83 | 84 | private BufferedWriter writer; 85 | 86 | @Override 87 | public synchronized BufferedWriter getWriter() throws IOException { 88 | if (writer == null) { 89 | // Unfortunately this means that we get two intermediate objects between the object we return 90 | // and the underlying Writer that response.getWriter() wraps. We could try accessing the 91 | // PrintWriter.out field via reflection, but that sort of access to non-public fields of 92 | // platform classes is now frowned on and may draw warnings or even fail in subsequent 93 | // versions. We could instead wrap the OutputStream, but that would require us to deduce the 94 | // appropriate Charset, using logic like this: 95 | // https://github.com/eclipse/jetty.project/blob/923ec38adf/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L731 96 | // We may end up doing that if performance is an issue. 97 | writer = new BufferedWriter(response.getWriter()); 98 | } 99 | return writer; 100 | } 101 | 102 | public void flush() { 103 | try { 104 | // We can't use HttpServletResponse.flushBuffer() because we wrap the 105 | // PrintWriter returned by HttpServletResponse in our own BufferedWriter 106 | // to match our API. So we have to flush whichever of getWriter() or 107 | // getOutputStream() works. 108 | try { 109 | getOutputStream().flush(); 110 | } catch (IllegalStateException e) { 111 | getWriter().flush(); 112 | } 113 | } catch (IOException e) { 114 | // Too bad, can't flush. 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /invoker/core/src/main/java/com/google/cloud/functions/invoker/http/TimeoutFilter.java: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.invoker.http; 16 | 17 | import java.io.IOException; 18 | import java.util.Timer; 19 | import java.util.TimerTask; 20 | import java.util.logging.Logger; 21 | import javax.servlet.Filter; 22 | import javax.servlet.FilterChain; 23 | import javax.servlet.ServletException; 24 | import javax.servlet.ServletRequest; 25 | import javax.servlet.ServletResponse; 26 | import javax.servlet.http.HttpServletResponse; 27 | 28 | public class TimeoutFilter implements Filter { 29 | 30 | private static final Logger logger = Logger.getLogger(TimeoutFilter.class.getName()); 31 | private final int timeoutMs; 32 | 33 | public TimeoutFilter(int timeoutSeconds) { 34 | this.timeoutMs = timeoutSeconds * 1000; // Convert seconds to milliseconds 35 | } 36 | 37 | @Override 38 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 39 | throws IOException, ServletException { 40 | Timer timer = new Timer(true); 41 | TimerTask timeoutTask = 42 | new TimerTask() { 43 | @Override 44 | public void run() { 45 | if (response instanceof HttpServletResponse) { 46 | try { 47 | ((HttpServletResponse) response) 48 | .sendError(HttpServletResponse.SC_REQUEST_TIMEOUT, "Request timed out"); 49 | } catch (IOException e) { 50 | logger.warning("Error while sending HTTP response: " + e.toString()); 51 | } 52 | } else { 53 | try { 54 | response.getWriter().write("Request timed out"); 55 | } catch (IOException e) { 56 | logger.warning("Error while writing response: " + e.toString()); 57 | } 58 | } 59 | } 60 | }; 61 | 62 | timer.schedule(timeoutTask, timeoutMs); 63 | 64 | try { 65 | chain.doFilter(request, response); 66 | timeoutTask.cancel(); 67 | } finally { 68 | timer.purge(); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/PackagelessHelloWorld.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // A function in the default package. 16 | 17 | import com.google.cloud.functions.HttpFunction; 18 | import com.google.cloud.functions.HttpRequest; 19 | import com.google.cloud.functions.HttpResponse; 20 | 21 | public class PackagelessHelloWorld implements HttpFunction { 22 | @Override 23 | public void service(HttpRequest request, HttpResponse response) throws Exception { 24 | response.setContentType("text/plain; charset=utf-8"); 25 | response.getWriter().write("hello, world\n"); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/BackgroundFunctionExecutorTest.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker; 2 | 3 | import static com.google.cloud.functions.invoker.BackgroundFunctionExecutor.backgroundFunctionTypeArgument; 4 | import static com.google.common.truth.Truth.assertThat; 5 | 6 | import com.google.cloud.functions.BackgroundFunction; 7 | import com.google.cloud.functions.Context; 8 | import com.google.gson.JsonObject; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.io.Reader; 12 | import java.util.Map; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.junit.runners.JUnit4; 16 | 17 | @RunWith(JUnit4.class) 18 | public class BackgroundFunctionExecutorTest { 19 | private static class PubSubMessage { 20 | String data; 21 | Map attributes; 22 | String messageId; 23 | String publishTime; 24 | } 25 | 26 | private static class PubSubFunction implements BackgroundFunction { 27 | @Override 28 | public void accept(PubSubMessage payload, Context context) {} 29 | } 30 | 31 | @Test 32 | public void backgroundFunctionTypeArgument_simple() { 33 | assertThat(backgroundFunctionTypeArgument(PubSubFunction.class)).hasValue(PubSubMessage.class); 34 | } 35 | 36 | private abstract static class Parent implements BackgroundFunction {} 37 | 38 | private static class Child extends Parent { 39 | @Override 40 | public void accept(PubSubMessage payload, Context context) {} 41 | } 42 | 43 | @Test 44 | public void backgroundFunctionTypeArgument_superclass() { 45 | assertThat(backgroundFunctionTypeArgument(Child.class)).hasValue(PubSubMessage.class); 46 | } 47 | 48 | private interface GenericParent extends BackgroundFunction {} 49 | 50 | private static class GenericChild implements GenericParent { 51 | @Override 52 | public void accept(PubSubMessage payload, Context context) {} 53 | } 54 | 55 | @Test 56 | public void backgroundFunctionTypeArgument_genericInterface() { 57 | assertThat(backgroundFunctionTypeArgument(GenericChild.class)).hasValue(PubSubMessage.class); 58 | } 59 | 60 | @SuppressWarnings("rawtypes") 61 | private static class ForgotTypeParameter implements BackgroundFunction { 62 | @Override 63 | public void accept(Object payload, Context context) {} 64 | } 65 | 66 | @Test 67 | public void backgroundFunctionTypeArgument_raw() { 68 | @SuppressWarnings("unchecked") 69 | Class> c = 70 | (Class>) (Class) ForgotTypeParameter.class; 71 | assertThat(backgroundFunctionTypeArgument(c)).isEmpty(); 72 | } 73 | 74 | @Test 75 | public void parseLegacyEventPubSub() throws IOException { 76 | try (Reader reader = 77 | new InputStreamReader(getClass().getResourceAsStream("/pubsub_background.json"))) { 78 | Event event = BackgroundFunctionExecutor.parseLegacyEvent(reader); 79 | 80 | Context context = event.getContext(); 81 | assertThat(context.eventType()).isEqualTo("google.pubsub.topic.publish"); 82 | assertThat(context.eventId()).isEqualTo("1"); 83 | assertThat(context.timestamp()).isEqualTo("2021-06-28T05:46:32.390Z"); 84 | 85 | JsonObject data = event.getData().getAsJsonObject(); 86 | assertThat(data.get("data").getAsString()).isEqualTo("eyJmb28iOiJiYXIifQ=="); 87 | String attr = data.get("attributes").getAsJsonObject().get("test").getAsString(); 88 | assertThat(attr).isEqualTo("123"); 89 | } 90 | } 91 | 92 | @Test 93 | public void parseLegacyEventPubSubEmulator() throws IOException { 94 | try (Reader reader = 95 | new InputStreamReader(getClass().getResourceAsStream("/pubsub_emulator.json"))) { 96 | Event event = BackgroundFunctionExecutor.parseLegacyEvent(reader); 97 | 98 | Context context = event.getContext(); 99 | assertThat(context.eventType()).isEqualTo("google.pubsub.topic.publish"); 100 | assertThat(context.eventId()).isEqualTo("1"); 101 | assertThat(context.timestamp()).isNotNull(); 102 | 103 | JsonObject data = event.getData().getAsJsonObject(); 104 | assertThat(data.get("data").getAsString()).isEqualTo("eyJmb28iOiJiYXIifQ=="); 105 | String attr = data.get("attributes").getAsJsonObject().get("test").getAsString(); 106 | assertThat(attr).isEqualTo("123"); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/CloudEventsTest.java: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.google.cloud.functions.invoker; 16 | 17 | import static com.google.common.truth.Truth.assertThat; 18 | import static com.google.common.truth.Truth.assertWithMessage; 19 | import static java.nio.charset.StandardCharsets.UTF_8; 20 | 21 | import io.cloudevents.CloudEvent; 22 | import io.cloudevents.jackson.JsonFormat; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.io.StringReader; 26 | import org.junit.Test; 27 | 28 | public class CloudEventsTest { 29 | @Test 30 | public void firebaseFirestoreTest() throws Exception { 31 | CloudEvent cloudEvent = cloudEventForResource("firestore_complex-cloudevent-input.json"); 32 | Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); 33 | 34 | Event expEvent = legacyEventForResource("firestore_complex-legacy-output.json"); 35 | assertThat(actualEvent).isEqualTo(expEvent); 36 | } 37 | 38 | @Test 39 | public void pubSubTest() throws Exception { 40 | CloudEvent cloudEvent = cloudEventForResource("pubsub_text-cloudevent-input.json"); 41 | Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); 42 | 43 | Event expEvent = legacyEventForResource("pubsub_text-legacy-output.json"); 44 | assertThat(actualEvent).isEqualTo(expEvent); 45 | } 46 | 47 | @Test 48 | public void firebaseAuthTest() throws Exception { 49 | CloudEvent cloudEvent = cloudEventForResource("firebase-auth-cloudevent-input.json"); 50 | Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); 51 | 52 | Event expEvent = legacyEventForResource("firebase-auth-legacy-output.json"); 53 | assertThat(actualEvent).isEqualTo(expEvent); 54 | } 55 | 56 | @Test 57 | public void firebaseDb1Test() throws Exception { 58 | CloudEvent cloudEvent = cloudEventForResource("firebase-db1-cloudevent-input.json"); 59 | Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); 60 | 61 | Event expEvent = legacyEventForResource("firebase-db1-legacy-output.json"); 62 | assertThat(actualEvent).isEqualTo(expEvent); 63 | } 64 | 65 | @Test 66 | public void firebaseDb2Test() throws Exception { 67 | CloudEvent cloudEvent = cloudEventForResource("firebase-db2-cloudevent-input.json"); 68 | Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); 69 | 70 | Event expEvent = legacyEventForResource("firebase-db2-legacy-output.json"); 71 | assertThat(actualEvent).isEqualTo(expEvent); 72 | } 73 | 74 | @Test 75 | public void storageTest() throws Exception { 76 | CloudEvent cloudEvent = cloudEventForResource("storage-cloudevent-input.json"); 77 | Event actualEvent = CloudEvents.convertToLegacyEvent(cloudEvent); 78 | 79 | Event expEvent = legacyEventForResource("storage-legacy-output.json"); 80 | assertThat(actualEvent).isEqualTo(expEvent); 81 | } 82 | 83 | private CloudEvent cloudEventForResource(String resourceName) throws IOException { 84 | try (InputStream in = getClass().getResourceAsStream("/" + resourceName)) { 85 | assertWithMessage("No such resource /%s", resourceName).that(in).isNotNull(); 86 | byte[] req = in.readAllBytes(); 87 | return io.cloudevents.core.provider.EventFormatProvider.getInstance() 88 | .resolveFormat(JsonFormat.CONTENT_TYPE) 89 | .deserialize(req); 90 | } 91 | } 92 | 93 | private Event legacyEventForResource(String resourceName) throws IOException { 94 | try (InputStream in = getClass().getResourceAsStream("/" + resourceName)) { 95 | assertWithMessage("No such resource /%s", resourceName).that(in).isNotNull(); 96 | String legacyEventString = new String(in.readAllBytes(), UTF_8); 97 | return BackgroundFunctionExecutor.parseLegacyEvent(new StringReader(legacyEventString)); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/HttpFunctionExecutorTest.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker; 2 | 3 | import static com.google.common.truth.Truth.assertThat; 4 | 5 | import com.google.cloud.functions.HttpFunction; 6 | import com.google.cloud.functions.HttpRequest; 7 | import com.google.cloud.functions.HttpResponse; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.junit.runners.JUnit4; 11 | 12 | @RunWith(JUnit4.class) 13 | public class HttpFunctionExecutorTest { 14 | private static ClassLoader customClassLoader = 15 | new ClassLoader(ClassLoader.getSystemClassLoader()) {}; 16 | 17 | public static class ClassLoaderVerifier implements HttpFunction { 18 | public ClassLoaderVerifier() { 19 | assertThat(Thread.currentThread().getContextClassLoader()) 20 | .isNotSameInstanceAs(customClassLoader); 21 | } 22 | 23 | @Override 24 | public void service(HttpRequest request, HttpResponse response) throws Exception { 25 | throw new UnsupportedOperationException("Not implemented"); 26 | } 27 | } 28 | 29 | @Test 30 | public void usesCorrectClassLoaderOverride() { 31 | ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); 32 | Thread.currentThread().setContextClassLoader(customClassLoader); 33 | HttpFunctionExecutor.forClass(ClassLoaderVerifier.class); 34 | Thread.currentThread().setContextClassLoader(oldClassLoader); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/TypedFunctionExecutorTest.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker; 2 | 3 | import static com.google.common.truth.Truth.assertThat; 4 | 5 | import com.google.cloud.functions.TypedFunction; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.junit.runners.JUnit4; 9 | 10 | @RunWith(JUnit4.class) 11 | public class TypedFunctionExecutorTest { 12 | private static class NameConcatRequest { 13 | String firstName; 14 | String lastName; 15 | } 16 | 17 | private static class NameConcatResponse { 18 | String fullName; 19 | } 20 | 21 | private static class NameConcatFunction 22 | implements TypedFunction { 23 | @Override 24 | public NameConcatResponse apply(NameConcatRequest arg) throws Exception { 25 | NameConcatResponse resp = new NameConcatResponse(); 26 | resp.fullName = arg.firstName + arg.lastName; 27 | return resp; 28 | } 29 | } 30 | 31 | @Test 32 | public void canDetermineTypeArgument() { 33 | assertThat(TypedFunctionExecutor.handlerTypeArgument(NameConcatFunction.class)) 34 | .hasValue(NameConcatRequest.class); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/runner/InvokerTest.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.runner; 2 | 3 | import static com.google.common.truth.Truth.assertThat; 4 | import static com.google.common.truth.Truth.assertWithMessage; 5 | import static java.util.stream.Collectors.joining; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.PrintStream; 11 | import java.net.URL; 12 | import java.net.URLClassLoader; 13 | import java.nio.charset.StandardCharsets; 14 | import java.util.Arrays; 15 | import java.util.Collections; 16 | import java.util.Map; 17 | import java.util.Optional; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | import org.junit.runners.JUnit4; 21 | 22 | @RunWith(JUnit4.class) 23 | public class InvokerTest { 24 | @Test 25 | public void help() throws IOException { 26 | String help = 27 | captureOutput( 28 | () -> { 29 | Optional invoker = Invoker.makeInvoker("--help"); 30 | assertThat(invoker).isEmpty(); 31 | }); 32 | assertThat(help).contains("Usage:"); 33 | assertThat(help).contains("--target"); 34 | assertThat(help).containsMatch("separated\\s+by\\s+'" + File.pathSeparator + "'"); 35 | } 36 | 37 | @Test 38 | public void defaultPort() { 39 | Optional invoker = Invoker.makeInvoker(); 40 | assertThat(invoker.get().getPort()).isEqualTo(8080); 41 | } 42 | 43 | @Test 44 | public void explicitPort() { 45 | Optional invoker = Invoker.makeInvoker("--port", "1234"); 46 | assertThat(invoker.get().getPort()).isEqualTo(1234); 47 | } 48 | 49 | @Test 50 | public void defaultTarget() { 51 | Optional invoker = Invoker.makeInvoker(); 52 | assertThat(invoker.get().getFunctionTarget()).isEqualTo("Function"); 53 | } 54 | 55 | @Test 56 | public void explicitTarget() { 57 | Optional invoker = Invoker.makeInvoker("--target", "com.example.MyFunction"); 58 | assertThat(invoker.get().getFunctionTarget()).isEqualTo("com.example.MyFunction"); 59 | } 60 | 61 | @Test 62 | public void defaultSignatureType() { 63 | Optional invoker = Invoker.makeInvoker(); 64 | assertThat(invoker.get().getFunctionSignatureType()).isNull(); 65 | } 66 | 67 | @Test 68 | public void explicitSignatureType() { 69 | Map env = Collections.singletonMap("FUNCTION_SIGNATURE_TYPE", "http"); 70 | Optional invoker = Invoker.makeInvoker(env); 71 | assertThat(invoker.get().getFunctionSignatureType()).isEqualTo("http"); 72 | } 73 | 74 | @Test 75 | public void defaultClasspath() { 76 | Optional invoker = Invoker.makeInvoker(); 77 | assertThat(invoker.get().getClass().getClassLoader()) 78 | .isSameInstanceAs(Invoker.class.getClassLoader()); 79 | } 80 | 81 | private static final String FAKE_CLASSPATH = 82 | "/foo/bar/baz.jar" + File.pathSeparator + "/some/directory"; 83 | 84 | @Test 85 | public void explicitClasspathViaEnvironment() { 86 | Map env = Collections.singletonMap("FUNCTION_CLASSPATH", FAKE_CLASSPATH); 87 | Optional invoker = Invoker.makeInvoker(env); 88 | assertThat(invokerClasspath(invoker.get())).isEqualTo(FAKE_CLASSPATH); 89 | } 90 | 91 | @Test 92 | public void explicitClasspathViaOption() { 93 | Optional invoker = Invoker.makeInvoker("--classpath", FAKE_CLASSPATH); 94 | assertThat(invokerClasspath(invoker.get())).isEqualTo(FAKE_CLASSPATH); 95 | } 96 | 97 | private static String invokerClasspath(Invoker invoker) { 98 | URLClassLoader urlClassLoader = (URLClassLoader) invoker.getFunctionClassLoader(); 99 | return Arrays.stream(urlClassLoader.getURLs()) 100 | .map(URL::getPath) 101 | .collect(joining(File.pathSeparator)); 102 | } 103 | 104 | @Test 105 | public void classpathToUrls() throws Exception { 106 | String classpath = 107 | "../testfunction/target/test-classes" + File.pathSeparator + "../testfunction/target/lib/*"; 108 | URL[] urls = Invoker.classpathToUrls(classpath); 109 | assertWithMessage(Arrays.toString(urls)).that(urls.length).isGreaterThan(2); 110 | File classesDir = new File(urls[0].toURI()); 111 | assertWithMessage(classesDir.toString()).that(classesDir.isDirectory()).isTrue(); 112 | for (int i = 1; i < urls.length; i++) { 113 | URL url = urls[i]; 114 | assertThat(url.toString()).endsWith(".jar"); 115 | assertWithMessage(url.toString()).that(new File(url.toURI()).isFile()).isTrue(); 116 | } 117 | } 118 | 119 | private static String captureOutput(Runnable operation) throws IOException { 120 | PrintStream originalOut = System.out; 121 | PrintStream originalErr = System.err; 122 | ByteArrayOutputStream byteCapture = new ByteArrayOutputStream(); 123 | try (PrintStream capture = new PrintStream(byteCapture)) { 124 | System.setOut(capture); 125 | System.setErr(capture); 126 | operation.run(); 127 | } finally { 128 | System.setOut(originalOut); 129 | System.setErr(originalErr); 130 | } 131 | return new String(byteCapture.toByteArray(), StandardCharsets.UTF_8); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/BackgroundSnoop.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.Context; 4 | import com.google.cloud.functions.RawBackgroundFunction; 5 | import com.google.gson.Gson; 6 | import com.google.gson.JsonObject; 7 | import java.io.FileWriter; 8 | import java.io.IOException; 9 | import java.io.PrintWriter; 10 | import java.io.UncheckedIOException; 11 | 12 | /** 13 | * Extract the targetFile property from the data of the JSON payload, and write to it a JSON 14 | * encoding of this payload and the context. The JSON format is chosen to be identical to the 15 | * EventFlow format that we currently use in GCF, and the file that we write should in fact be 16 | * identical to the JSON payload that the Functions Framework received from the client in the test. 17 | * This will need to be rewritten when we switch to CloudEvents. 18 | */ 19 | public class BackgroundSnoop implements RawBackgroundFunction { 20 | @Override 21 | public void accept(String json, Context context) { 22 | Gson gson = new Gson(); 23 | JsonObject jsonObject = gson.fromJson(json, JsonObject.class); 24 | String targetFile = jsonObject.get("targetFile").getAsString(); 25 | if (targetFile == null) { 26 | throw new IllegalArgumentException("Expected targetFile in JSON payload"); 27 | } 28 | JsonObject resourceJson = gson.fromJson(context.resource(), JsonObject.class); 29 | JsonObject contextJson = new JsonObject(); 30 | contextJson.addProperty("eventId", context.eventId()); 31 | contextJson.addProperty("timestamp", context.timestamp()); 32 | contextJson.addProperty("eventType", context.eventType()); 33 | contextJson.add("resource", resourceJson); 34 | JsonObject contextAndPayloadJson = new JsonObject(); 35 | contextAndPayloadJson.add("data", jsonObject); 36 | contextAndPayloadJson.add("context", contextJson); 37 | try (FileWriter fileWriter = new FileWriter(targetFile); 38 | PrintWriter writer = new PrintWriter(fileWriter)) { 39 | writer.println(contextAndPayloadJson); 40 | } catch (IOException e) { 41 | throw new UncheckedIOException(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/CloudEventSnoop.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import static java.nio.charset.StandardCharsets.UTF_8; 4 | 5 | import com.google.cloud.functions.CloudEventsFunction; 6 | import com.google.gson.Gson; 7 | import com.google.gson.JsonObject; 8 | import io.cloudevents.CloudEvent; 9 | import io.cloudevents.core.format.EventFormat; 10 | import io.cloudevents.core.provider.EventFormatProvider; 11 | import io.cloudevents.jackson.JsonFormat; 12 | import java.io.FileOutputStream; 13 | 14 | public class CloudEventSnoop implements CloudEventsFunction { 15 | @Override 16 | public void accept(CloudEvent event) throws Exception { 17 | String payloadJson = new String(event.getData().toBytes(), UTF_8); 18 | Gson gson = new Gson(); 19 | JsonObject jsonObject = gson.fromJson(payloadJson, JsonObject.class); 20 | String targetFile = jsonObject.get("targetFile").getAsString(); 21 | if (targetFile == null) { 22 | throw new IllegalArgumentException("Expected targetFile in JSON payload"); 23 | } 24 | EventFormat jsonFormat = 25 | EventFormatProvider.getInstance().resolveFormat(JsonFormat.CONTENT_TYPE); 26 | byte[] bytes = jsonFormat.serialize(event); 27 | try (FileOutputStream out = new FileOutputStream(targetFile)) { 28 | out.write(bytes); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Echo.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpResponse; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.util.stream.Collectors; 9 | 10 | public class Echo implements HttpFunction { 11 | @Override 12 | public void service(HttpRequest request, HttpResponse response) throws Exception { 13 | boolean binary = "application/octet-stream".equals(request.getContentType().orElse(null)); 14 | if (binary) { 15 | response.setContentType("application/octet-stream"); 16 | byte[] buf = new byte[1024]; 17 | InputStream in = request.getInputStream(); 18 | OutputStream out = response.getOutputStream(); 19 | int n; 20 | while ((n = in.read(buf)) > 0) { 21 | out.write(buf, 0, n); 22 | } 23 | } else { 24 | String body = request.getReader().lines().collect(Collectors.joining("\n")) + "\n"; 25 | response.setContentType("text/plain"); 26 | response.getWriter().write(body); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/EchoUrl.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpResponse; 6 | 7 | public class EchoUrl implements HttpFunction { 8 | @Override 9 | public void service(HttpRequest request, HttpResponse response) throws Exception { 10 | StringBuilder url = new StringBuilder(request.getPath()); 11 | request.getQuery().ifPresent(q -> url.append("?").append(q)); 12 | url.append("\n"); 13 | response.getWriter().write(url.toString()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionBackground.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.Context; 4 | import com.google.cloud.functions.RawBackgroundFunction; 5 | 6 | public class ExceptionBackground implements RawBackgroundFunction { 7 | @Override 8 | public void accept(String json, Context context) { 9 | throw new RuntimeException("exception thrown for test"); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/ExceptionHttp.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpResponse; 6 | 7 | public class ExceptionHttp implements HttpFunction { 8 | @Override 9 | public void service(HttpRequest request, HttpResponse response) throws Exception { 10 | throw new RuntimeException("exception thrown for test"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/HelloWorld.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpResponse; 6 | 7 | public class HelloWorld implements HttpFunction { 8 | @Override 9 | public void service(HttpRequest request, HttpResponse response) throws Exception { 10 | response.getWriter().write("hello\n"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Log.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpResponse; 6 | import java.lang.reflect.Field; 7 | import java.util.Optional; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | /** Emit log messages with configurable level, message, and exception. */ 12 | public class Log implements HttpFunction { 13 | private static final Logger logger = Logger.getLogger(Log.class.getName()); 14 | 15 | @Override 16 | public void service(HttpRequest request, HttpResponse response) throws Exception { 17 | String message = request.getFirstQueryParameter("message").orElse("Default message"); 18 | String levelString = request.getFirstQueryParameter("level").orElse("info"); 19 | Optional exceptionString = request.getFirstQueryParameter("exception"); 20 | Field levelField = Level.class.getField(levelString.toUpperCase()); 21 | Level level = (Level) levelField.get(null); 22 | if (exceptionString.isPresent()) { 23 | logger.log(level, message, new Exception(exceptionString.get())); 24 | } else { 25 | logger.log(level, message); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Multipart.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpRequest.HttpPart; 6 | import com.google.cloud.functions.HttpResponse; 7 | import java.io.PrintWriter; 8 | import java.util.NavigableMap; 9 | import java.util.TreeMap; 10 | 11 | /** 12 | * A simple proof-of-concept function for multipart handling. 13 | * 14 | *

{@code HttpTest} contains more detailed testing, but this function is part of the integration 15 | * test that shows that we can indeed access the multipart API from a function. 16 | */ 17 | public class Multipart implements HttpFunction { 18 | @Override 19 | public void service(HttpRequest request, HttpResponse response) throws Exception { 20 | response.setContentType("text/plain"); 21 | String contentType = request.getContentType().orElse(""); 22 | if (!contentType.startsWith("multipart/form-data")) { 23 | response.getWriter().write("Content-Type is " + contentType + " not multipart/form-data"); 24 | return; 25 | } 26 | PrintWriter writer = new PrintWriter(response.getWriter()); 27 | NavigableMap parts = new TreeMap<>(request.getParts()); 28 | parts.forEach( 29 | (name, contents) -> { 30 | writer.printf( 31 | "part %s type %s length %d\n", 32 | name, contents.getContentType().get(), contents.getContentLength()); 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Nested.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpResponse; 6 | import java.util.stream.Collectors; 7 | 8 | public class Nested { 9 | public static class Echo implements HttpFunction { 10 | @Override 11 | public void service(HttpRequest request, HttpResponse response) throws Exception { 12 | String body = request.getReader().lines().collect(Collectors.joining("\n")); 13 | response.setContentType("text/plain"); 14 | response.getWriter().write(body); 15 | response.getWriter().flush(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/TimeoutHttp.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpFunction; 4 | import com.google.cloud.functions.HttpRequest; 5 | import com.google.cloud.functions.HttpResponse; 6 | 7 | public class TimeoutHttp implements HttpFunction { 8 | 9 | @Override 10 | public void service(HttpRequest request, HttpResponse response) throws Exception { 11 | try { 12 | Thread.sleep(2000); 13 | } catch (InterruptedException e) { 14 | response.getWriter().close(); 15 | } 16 | response.getWriter().write("finished\n"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/Typed.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.TypedFunction; 4 | 5 | public class Typed implements TypedFunction { 6 | 7 | @Override 8 | public NameConcatResponse apply(NameConcatRequest arg) throws Exception { 9 | return new NameConcatResponse().setFullName(arg.firstName + arg.lastName); 10 | } 11 | } 12 | 13 | class NameConcatRequest { 14 | String firstName; 15 | String lastName; 16 | } 17 | 18 | class NameConcatResponse { 19 | String fullName; 20 | 21 | NameConcatResponse setFullName(String fullName) { 22 | this.fullName = fullName; 23 | return this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/TypedBackgroundSnoop.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.BackgroundFunction; 4 | import com.google.cloud.functions.Context; 5 | import com.google.gson.Gson; 6 | import com.google.gson.JsonObject; 7 | import java.io.FileWriter; 8 | import java.io.IOException; 9 | import java.io.PrintWriter; 10 | import java.io.UncheckedIOException; 11 | 12 | /** 13 | * Extract the targetFile property from the data of the JSON payload, and write to it a JSON 14 | * encoding of this payload and the context. The JSON format is chosen to be identical to the 15 | * EventFlow format that we currently use in GCF, and the file that we write should in fact be 16 | * identical to the JSON payload that the Functions Framework received from the client in the test. 17 | * This will need to be rewritten when we switch to CloudEvents. 18 | */ 19 | public class TypedBackgroundSnoop implements BackgroundFunction { 20 | public static class Payload { 21 | public int a; 22 | public int b; 23 | public String targetFile; 24 | } 25 | 26 | @Override 27 | public void accept(Payload payload, Context context) { 28 | Gson gson = new Gson(); 29 | String targetFile = payload.targetFile; 30 | if (targetFile == null) { 31 | throw new IllegalArgumentException("Expected targetFile in JSON payload"); 32 | } 33 | JsonObject resourceJson = gson.fromJson(context.resource(), JsonObject.class); 34 | JsonObject contextJson = new JsonObject(); 35 | contextJson.addProperty("eventId", context.eventId()); 36 | contextJson.addProperty("timestamp", context.timestamp()); 37 | contextJson.addProperty("eventType", context.eventType()); 38 | contextJson.add("resource", resourceJson); 39 | JsonObject contextAndPayloadJson = new JsonObject(); 40 | contextAndPayloadJson.add("data", gson.toJsonTree(payload)); 41 | contextAndPayloadJson.add("context", contextJson); 42 | try (FileWriter fileWriter = new FileWriter(targetFile); 43 | PrintWriter writer = new PrintWriter(fileWriter)) { 44 | writer.println(contextAndPayloadJson); 45 | } catch (IOException e) { 46 | throw new UncheckedIOException(e); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/TypedCustomFormat.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.HttpRequest; 4 | import com.google.cloud.functions.HttpResponse; 5 | import com.google.cloud.functions.TypedFunction; 6 | import java.lang.reflect.Type; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class TypedCustomFormat implements TypedFunction, String> { 11 | 12 | @Override 13 | public String apply(List arg) throws Exception { 14 | return String.join("", arg); 15 | } 16 | 17 | @Override 18 | public WireFormat getWireFormat() { 19 | return new CustomFormat(); 20 | } 21 | } 22 | 23 | class CustomFormat implements TypedFunction.WireFormat { 24 | @Override 25 | public Object deserialize(HttpRequest request, Type type) throws Exception { 26 | List req = new ArrayList<>(); 27 | String line; 28 | while ((line = request.getReader().readLine()) != null) { 29 | req.add(line); 30 | } 31 | return req; 32 | } 33 | 34 | @Override 35 | public void serialize(Object object, HttpResponse response) throws Exception { 36 | response.getWriter().write((String) object); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /invoker/core/src/test/java/com/google/cloud/functions/invoker/testfunctions/TypedVoid.java: -------------------------------------------------------------------------------- 1 | package com.google.cloud.functions.invoker.testfunctions; 2 | 3 | import com.google.cloud.functions.TypedFunction; 4 | 5 | public class TypedVoid implements TypedFunction { 6 | @Override 7 | public Void apply(Request arg) throws Exception { 8 | return null; 9 | } 10 | } 11 | 12 | class Request {} 13 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/adder_gcf_ga_event.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "a": 2, 4 | "b": 3 5 | }, 6 | "context": { 7 | "eventId": "B234-1234-1234", 8 | "timestamp": "2018-04-05T17:31:00Z", 9 | "eventType": "com.example.someevent.new", 10 | "resource": { 11 | "service":"test-service", 12 | "name":"test-name", 13 | "type":"test-type" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-auth-cloudevent-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion": "1.0", 3 | "type": "google.firebase.auth.user.v1.created", 4 | "source": "//firebaseauth.googleapis.com/projects/my-project-id", 5 | "subject": "users/UUpby3s4spZre6kHsgVSPetzQ8l2", 6 | "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", 7 | "time": "2020-09-29T11:32:00.123Z", 8 | "datacontenttype": "application/json", 9 | "data": { 10 | "email": "test@nowhere.com", 11 | "metadata": { 12 | "createTime": "2020-05-26T10:42:27Z", 13 | "lastSignInTime": "2020-10-24T11:00:00Z" 14 | }, 15 | "providerData": [ 16 | { 17 | "email": "test@nowhere.com", 18 | "providerId": "password", 19 | "uid": "test@nowhere.com" 20 | } 21 | ], 22 | "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" 23 | } 24 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-auth-legacy-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "email": "test@nowhere.com", 4 | "metadata": { 5 | "createdAt": "2020-05-26T10:42:27Z", 6 | "lastSignedInAt": "2020-10-24T11:00:00Z" 7 | }, 8 | "providerData": [ 9 | { 10 | "email": "test@nowhere.com", 11 | "providerId": "password", 12 | "uid": "test@nowhere.com" 13 | } 14 | ], 15 | "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" 16 | }, 17 | "context": { 18 | "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", 19 | "eventType": "providers/firebase.auth/eventTypes/user.create", 20 | "resource": "projects/my-project-id", 21 | "timestamp": "2020-09-29T11:32:00.123Z" 22 | } 23 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-auth1.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "email": "test@nowhere.com", 4 | "metadata": { 5 | "createdAt": "2020-05-26T10:42:27Z" 6 | }, 7 | "providerData": [ 8 | { 9 | "email": "test@nowhere.com", 10 | "providerId": "password", 11 | "uid": "test@nowhere.com" 12 | } 13 | ], 14 | "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" 15 | }, 16 | "eventId": "4423b4fa-c39b-4f79-b338-977a018e9b55", 17 | "eventType": "providers/firebase.auth/eventTypes/user.create", 18 | "notSupported": { 19 | }, 20 | "resource": "projects/my-project-id", 21 | "timestamp": "2020-05-26T10:42:27.088Z" 22 | } 23 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-auth2.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "email": "test@nowhere.com", 4 | "metadata": { 5 | "createdAt": "2020-05-26T10:42:27Z" 6 | }, 7 | "providerData": [ 8 | { 9 | "email": "test@nowhere.com", 10 | "providerId": "password", 11 | "uid": "test@nowhere.com" 12 | } 13 | ], 14 | "uid": "UUpby3s4spZre6kHsgVSPetzQ8l2" 15 | }, 16 | "eventId": "5fd71bdc-4955-421f-9fc3-552ac3abead8", 17 | "eventType": "providers/firebase.auth/eventTypes/user.delete", 18 | "notSupported": { 19 | }, 20 | "resource": "projects/my-project-id", 21 | "timestamp": "2020-05-26T10:47:14.205Z" 22 | } 23 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-db1-cloudevent-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion": "1.0", 3 | "type": "google.firebase.database.ref.v1.written", 4 | "source": "//firebasedatabase.googleapis.com/projects/_/locations/us-central1/instances/my-project-id", 5 | "subject": "refs/gcf-test/xyz", 6 | "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", 7 | "time": "2020-09-29T11:32:00.123Z", 8 | "datacontenttype": "application/json", 9 | "data": { 10 | "data": null, 11 | "delta": { 12 | "grandchild": "other" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-db1-legacy-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "data": null, 4 | "delta": { 5 | "grandchild": "other" 6 | } 7 | }, 8 | "context": { 9 | "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", 10 | "timestamp": "2020-09-29T11:32:00.123Z", 11 | "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", 12 | "eventType": "providers/google.firebase.database/eventTypes/ref.write" 13 | } 14 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-db1.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventType": "providers/google.firebase.database/eventTypes/ref.write", 3 | "params": { 4 | "child": "xyz" 5 | }, 6 | "auth": { 7 | "admin": true 8 | }, 9 | "domain": "firebaseio.com", 10 | "data": { 11 | "data": null, 12 | "delta": { 13 | "grandchild": "other" 14 | } 15 | }, 16 | "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", 17 | "timestamp": "2020-05-21T11:15:34.178Z", 18 | "eventId": "/SnHth9OSlzK1Puj85kk4tDbF90=" 19 | } 20 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-db2-cloudevent-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion": "1.0", 3 | "type": "google.firebase.database.ref.v1.written", 4 | "source": "//firebasedatabase.googleapis.com/projects/_/locations/europe-west1/instances/my-project-id", 5 | "subject": "refs/gcf-test/xyz", 6 | "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", 7 | "time": "2020-09-29T11:32:00.123Z", 8 | "datacontenttype": "application/json", 9 | "data": { 10 | "data": { 11 | "grandchild": "other" 12 | }, 13 | "delta": { 14 | "grandchild": "other changed" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-db2-legacy-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "data": { 4 | "grandchild": "other" 5 | }, 6 | "delta": { 7 | "grandchild": "other changed" 8 | } 9 | }, 10 | "context": { 11 | "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", 12 | "timestamp": "2020-09-29T11:32:00.123Z", 13 | "eventType": "providers/google.firebase.database/eventTypes/ref.write", 14 | "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firebase-db2.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventType": "providers/google.firebase.database/eventTypes/ref.write", 3 | "params": { 4 | "child": "xyz" 5 | }, 6 | "auth": { 7 | "admin": true 8 | }, 9 | "domain":"europe-west1.firebasedatabase.app", 10 | "data": { 11 | "data": { 12 | "grandchild": "other" 13 | }, 14 | "delta": { 15 | "grandchild": "other changed" 16 | } 17 | }, 18 | "resource": "projects/_/instances/my-project-id/refs/gcf-test/xyz", 19 | "timestamp": "2020-09-29T11:32:00.000Z", 20 | "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc" 21 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firestore_complex-cloudevent-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion": "1.0", 3 | "type": "google.cloud.firestore.document.v1.written", 4 | "source": "//firestore.googleapis.com/projects/project-id/databases/(default)", 5 | "subject": "documents/gcf-test/IH75dRdeYJKd4uuQiqch", 6 | "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", 7 | "time": "2020-09-29T11:32:00.123Z", 8 | "datacontenttype": "application/json", 9 | "data": { 10 | "oldValue": {}, 11 | "updateMask": {}, 12 | "value": { 13 | "createTime": "2020-04-23T14:25:05.349632Z", 14 | "fields": { 15 | "arrayValue": { 16 | "arrayValue": { 17 | "values": [ 18 | { 19 | "integerValue": "1" 20 | }, 21 | { 22 | "integerValue": "2" 23 | } 24 | ] 25 | } 26 | }, 27 | "booleanValue": { 28 | "booleanValue": true 29 | }, 30 | "doubleValue": { 31 | "doubleValue": 5.5 32 | }, 33 | "geoPointValue": { 34 | "geoPointValue": { 35 | "latitude": 51.4543, 36 | "longitude": -0.9781 37 | } 38 | }, 39 | "intValue": { 40 | "integerValue": "50" 41 | }, 42 | "mapValue": { 43 | "mapValue": { 44 | "fields": { 45 | "field1": { 46 | "stringValue": "x" 47 | }, 48 | "field2": { 49 | "arrayValue": { 50 | "values": [ 51 | { 52 | "stringValue": "x" 53 | }, 54 | { 55 | "integerValue": "1" 56 | } 57 | ] 58 | } 59 | } 60 | } 61 | } 62 | }, 63 | "nullValue": { 64 | "nullValue": null 65 | }, 66 | "referenceValue": { 67 | "referenceValue": "projects/project-id/databases/(default)/documents/foo/bar/baz/qux" 68 | }, 69 | "stringValue": { 70 | "stringValue": "text" 71 | }, 72 | "timestampValue": { 73 | "timestampValue": "2020-04-23T14:23:53.241Z" 74 | } 75 | }, 76 | "name": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", 77 | "updateTime": "2020-04-23T14:25:05.349632Z" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firestore_complex-legacy-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "oldValue": {}, 4 | "updateMask": {}, 5 | "value": { 6 | "createTime": "2020-04-23T14:25:05.349632Z", 7 | "fields": { 8 | "arrayValue": { 9 | "arrayValue": { 10 | "values": [ 11 | { 12 | "integerValue": "1" 13 | }, 14 | { 15 | "integerValue": "2" 16 | } 17 | ] 18 | } 19 | }, 20 | "booleanValue": { 21 | "booleanValue": true 22 | }, 23 | "doubleValue": { 24 | "doubleValue": 5.5 25 | }, 26 | "geoPointValue": { 27 | "geoPointValue": { 28 | "latitude": 51.4543, 29 | "longitude": -0.9781 30 | } 31 | }, 32 | "intValue": { 33 | "integerValue": "50" 34 | }, 35 | "mapValue": { 36 | "mapValue": { 37 | "fields": { 38 | "field1": { 39 | "stringValue": "x" 40 | }, 41 | "field2": { 42 | "arrayValue": { 43 | "values": [ 44 | { 45 | "stringValue": "x" 46 | }, 47 | { 48 | "integerValue": "1" 49 | } 50 | ] 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | "nullValue": { 57 | "nullValue": null 58 | }, 59 | "referenceValue": { 60 | "referenceValue": "projects/project-id/databases/(default)/documents/foo/bar/baz/qux" 61 | }, 62 | "stringValue": { 63 | "stringValue": "text" 64 | }, 65 | "timestampValue": { 66 | "timestampValue": "2020-04-23T14:23:53.241Z" 67 | } 68 | }, 69 | "name": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", 70 | "updateTime": "2020-04-23T14:25:05.349632Z" 71 | } 72 | }, 73 | "context": { 74 | "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", 75 | "eventType": "providers/cloud.firestore/eventTypes/document.write", 76 | "resource": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", 77 | "timestamp": "2020-09-29T11:32:00.123Z" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firestore_complex.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "oldValue": {}, 4 | "updateMask": {}, 5 | "value": { 6 | "createTime": "2020-04-23T14:25:05.349632Z", 7 | "fields": { 8 | "arrayValue": { 9 | "arrayValue": { 10 | "values": [ 11 | { 12 | "integerValue": "1" 13 | }, 14 | { 15 | "integerValue": "2" 16 | } 17 | ] 18 | } 19 | }, 20 | "booleanValue": { 21 | "booleanValue": true 22 | }, 23 | "doubleValue": { 24 | "doubleValue": 5.5 25 | }, 26 | "geoPointValue": { 27 | "geoPointValue": { 28 | "latitude": 51.4543, 29 | "longitude": -0.9781 30 | } 31 | }, 32 | "intValue": { 33 | "integerValue": "50" 34 | }, 35 | "mapValue": { 36 | "mapValue": { 37 | "fields": { 38 | "field1": { 39 | "stringValue": "x" 40 | }, 41 | "field2": { 42 | "arrayValue": { 43 | "values": [ 44 | { 45 | "stringValue": "x" 46 | }, 47 | { 48 | "integerValue": "1" 49 | } 50 | ] 51 | } 52 | } 53 | } 54 | } 55 | }, 56 | "nullValue": { 57 | "nullValue": null 58 | }, 59 | "referenceValue": { 60 | "referenceValue": "projects/project-id/databases/(default)/documents/foo/bar/baz/qux" 61 | }, 62 | "stringValue": { 63 | "stringValue": "text" 64 | }, 65 | "timestampValue": { 66 | "timestampValue": "2020-04-23T14:23:53.241Z" 67 | } 68 | }, 69 | "name": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", 70 | "updateTime": "2020-04-23T14:25:05.349632Z" 71 | } 72 | }, 73 | "eventId": "9babded5-e5f2-41af-a46a-06ba6bd84739-0", 74 | "eventType": "providers/cloud.firestore/eventTypes/document.write", 75 | "notSupported": {}, 76 | "params": { 77 | "doc": "IH75dRdeYJKd4uuQiqch" 78 | }, 79 | "resource": "projects/project-id/databases/(default)/documents/gcf-test/IH75dRdeYJKd4uuQiqch", 80 | "timestamp": "2020-04-23T14:25:05.349632Z" 81 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/firestore_simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "data":{ 3 | "oldValue":{ 4 | "createTime":"2020-04-23T09:58:53.211035Z", 5 | "fields":{ 6 | "another test":{ 7 | "stringValue":"asd" 8 | }, 9 | "count":{ 10 | "integerValue":"3" 11 | }, 12 | "foo":{ 13 | "stringValue":"bar" 14 | } 15 | }, 16 | "name":"projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", 17 | "updateTime":"2020-04-23T12:00:27.247187Z" 18 | }, 19 | "updateMask":{ 20 | "fieldPaths":[ 21 | "count" 22 | ] 23 | }, 24 | "value":{ 25 | "createTime":"2020-04-23T09:58:53.211035Z", 26 | "fields":{ 27 | "another test":{ 28 | "stringValue":"asd" 29 | }, 30 | "count":{ 31 | "integerValue":"4" 32 | }, 33 | "foo":{ 34 | "stringValue":"bar" 35 | } 36 | }, 37 | "name":"projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", 38 | "updateTime":"2020-04-23T12:00:27.247187Z" 39 | } 40 | }, 41 | "eventId":"7b8f1804-d38b-4b68-b37d-e2fb5d12d5a0-0", 42 | "eventType":"providers/cloud.firestore/eventTypes/document.write", 43 | "notSupported":{ 44 | 45 | }, 46 | "params":{ 47 | "doc":"2Vm2mI1d0wIaK2Waj5to" 48 | }, 49 | "resource":"projects/project-id/databases/(default)/documents/gcf-test/2Vm2mI1d0wIaK2Waj5to", 50 | "timestamp":"2020-04-23T12:00:27.247187Z" 51 | } 52 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/legacy_pubsub.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventId": "1215011316659232", 3 | "timestamp": "2020-05-18T12:13:19.209Z", 4 | "eventType": "providers/cloud.pubsub/eventTypes/topic.publish", 5 | "resource": "projects/sample-project/topics/gcf-test", 6 | "data": { 7 | "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", 8 | "attributes": { 9 | "attribute1": "value1" 10 | }, 11 | "data": "VGhpcyBpcyBhIHNhbXBsZSBtZXNzYWdl" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/legacy_storage_change.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "bucket": "sample-bucket", 4 | "crc32c": "AAAAAA==", 5 | "etag": "COu8mb3Dn+kCEAE=", 6 | "generation": "1588778055917163", 7 | "id": "sample-bucket/MyFile/1588778055917163", 8 | "kind": "storage#object", 9 | "md5Hash": "ZDQxZDhjZDk4ZjAwYjIwNGU5ODAwOTk4ZWNmODQyN2U=", 10 | "mediaLink": "https://www.googleapis.com/download/storage/v1/b/projectid-sample-bucket/o/MyFile?generation=1588778055917163\u0026alt=media", 11 | "metageneration": "1", 12 | "name": "MyFile", 13 | "resourceState": "not_exists", 14 | "selfLink": "https://www.googleapis.com/storage/v1/b/projectid-sample-bucket/o/MyFile", 15 | "size": "0", 16 | "storageClass": "MULTI_REGIONAL", 17 | "timeCreated": "2020-05-06T15:14:15.917Z", 18 | "timeDeleted": "2020-05-18T09:07:51.799Z", 19 | "timeStorageClassUpdated": "2020-05-06T15:14:15.917Z", 20 | "updated": "2020-05-06T15:14:15.917Z" 21 | }, 22 | "eventId": "1200401551653202", 23 | "eventType": "providers/cloud.storage/eventTypes/object.change", 24 | "resource": "projects/_/buckets/sample-bucket/objects/MyFile#1588778055917163", 25 | "timestamp": "2020-05-18T09:07:51.799Z" 26 | } 27 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/pubsub_background.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", 4 | "data": "eyJmb28iOiJiYXIifQ==", 5 | "attributes": { 6 | "test": "123" 7 | } 8 | }, 9 | "context": { 10 | "eventId": "1", 11 | "eventType": "google.pubsub.topic.publish", 12 | "resource": { 13 | "name": "projects/FOO/topics/BAR_TOPIC", 14 | "service": "pubsub.googleapis.com", 15 | "type": "type.googleapis.com/google.pubsub.v1.PubsubMessage" 16 | }, 17 | "timestamp": "2021-06-28T05:46:32.390Z" 18 | } 19 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/pubsub_binary.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "eventId":"1144231683168617", 4 | "timestamp":"2020-05-06T07:33:34.556Z", 5 | "eventType":"google.pubsub.topic.publish", 6 | "resource":{ 7 | "service":"pubsub.googleapis.com", 8 | "name":"projects/sample-project/topics/gcf-test", 9 | "type":"type.googleapis.com/google.pubsub.v1.PubsubMessage" 10 | } 11 | }, 12 | "data": { 13 | "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", 14 | "data": "AQIDBA==" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/pubsub_emulator.json: -------------------------------------------------------------------------------- 1 | { 2 | "subscription": "projects/FOO/subscriptions/BAR_SUB", 3 | "message": { 4 | "data": "eyJmb28iOiJiYXIifQ==", 5 | "messageId": "1", 6 | "attributes": { 7 | "test": "123" 8 | } 9 | } 10 | } -------------------------------------------------------------------------------- /invoker/core/src/test/resources/pubsub_text-cloudevent-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion": "1.0", 3 | "type": "google.cloud.pubsub.topic.v1.messagePublished", 4 | "source": "//pubsub.googleapis.com/projects/sample-project/topics/gcf-test", 5 | "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", 6 | "time": "2020-09-29T11:32:00.123Z", 7 | "datacontenttype": "application/json", 8 | "data": { 9 | "subscription": "projects/sample-project/subscriptions/sample-subscription", 10 | "message": { 11 | "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", 12 | "messageId": "aaaaaa-1111-bbbb-2222-cccccccccccc", 13 | "publishTime": "2020-09-29T11:32:00.123Z", 14 | "attributes": { 15 | "attr1":"attr1-value" 16 | }, 17 | "data": "dGVzdCBtZXNzYWdlIDM=" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/pubsub_text-legacy-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "eventId":"aaaaaa-1111-bbbb-2222-cccccccccccc", 4 | "timestamp":"2020-09-29T11:32:00.123Z", 5 | "eventType":"google.pubsub.topic.publish", 6 | "resource":{ 7 | "service":"pubsub.googleapis.com", 8 | "name":"projects/sample-project/topics/gcf-test", 9 | "type":"type.googleapis.com/google.pubsub.v1.PubsubMessage" 10 | } 11 | }, 12 | "data": { 13 | "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", 14 | "attributes": { 15 | "attr1":"attr1-value" 16 | }, 17 | "data": "dGVzdCBtZXNzYWdlIDM=" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/pubsub_text.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "eventId":"1144231683168617", 4 | "timestamp":"2020-05-06T07:33:34.556Z", 5 | "eventType":"google.pubsub.topic.publish", 6 | "resource":{ 7 | "service":"pubsub.googleapis.com", 8 | "name":"projects/sample-project/topics/gcf-test", 9 | "type":"type.googleapis.com/google.pubsub.v1.PubsubMessage" 10 | } 11 | }, 12 | "data": { 13 | "@type": "type.googleapis.com/google.pubsub.v1.PubsubMessage", 14 | "attributes": { 15 | "attr1":"attr1-value" 16 | }, 17 | "data": "dGVzdCBtZXNzYWdlIDM=" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/storage-cloudevent-input.json: -------------------------------------------------------------------------------- 1 | { 2 | "specversion": "1.0", 3 | "type": "google.cloud.storage.object.v1.finalized", 4 | "source": "//storage.googleapis.com/projects/_/buckets/some-bucket", 5 | "subject": "objects/folder/Test.cs", 6 | "id": "aaaaaa-1111-bbbb-2222-cccccccccccc", 7 | "time": "2020-09-29T11:32:00.123Z", 8 | "datacontenttype": "application/json", 9 | "data": { 10 | "bucket": "some-bucket", 11 | "contentType": "text/plain", 12 | "crc32c": "rTVTeQ==", 13 | "etag": "CNHZkbuF/ugCEAE=", 14 | "generation": "1587627537231057", 15 | "id": "some-bucket/folder/Test.cs/1587627537231057", 16 | "kind": "storage#object", 17 | "md5Hash": "kF8MuJ5+CTJxvyhHS1xzRg==", 18 | "mediaLink": "https://www.googleapis.com/download/storage/v1/b/some-bucket/o/folder%2FTest.cs?generation=1587627537231057\u0026alt=media", 19 | "metageneration": "1", 20 | "name": "folder/Test.cs", 21 | "selfLink": "https://www.googleapis.com/storage/v1/b/some-bucket/o/folder/Test.cs", 22 | "size": "352", 23 | "storageClass": "MULTI_REGIONAL", 24 | "timeCreated": "2020-04-23T07:38:57.230Z", 25 | "timeStorageClassUpdated": "2020-04-23T07:38:57.230Z", 26 | "updated": "2020-04-23T07:38:57.230Z" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/storage-legacy-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "eventId": "aaaaaa-1111-bbbb-2222-cccccccccccc", 4 | "timestamp": "2020-09-29T11:32:00.123Z", 5 | "eventType": "google.storage.object.finalize", 6 | "resource": { 7 | "service": "storage.googleapis.com", 8 | "name": "projects/_/buckets/some-bucket/objects/folder/Test.cs", 9 | "type": "storage#object" 10 | } 11 | }, 12 | "data": { 13 | "bucket": "some-bucket", 14 | "contentType": "text/plain", 15 | "crc32c": "rTVTeQ==", 16 | "etag": "CNHZkbuF/ugCEAE=", 17 | "generation": "1587627537231057", 18 | "id": "some-bucket/folder/Test.cs/1587627537231057", 19 | "kind": "storage#object", 20 | "md5Hash": "kF8MuJ5+CTJxvyhHS1xzRg==", 21 | "mediaLink": "https://www.googleapis.com/download/storage/v1/b/some-bucket/o/folder%2FTest.cs?generation=1587627537231057\u0026alt=media", 22 | "metageneration": "1", 23 | "name": "folder/Test.cs", 24 | "selfLink": "https://www.googleapis.com/storage/v1/b/some-bucket/o/folder/Test.cs", 25 | "size": "352", 26 | "storageClass": "MULTI_REGIONAL", 27 | "timeCreated": "2020-04-23T07:38:57.230Z", 28 | "timeStorageClassUpdated": "2020-04-23T07:38:57.230Z", 29 | "updated": "2020-04-23T07:38:57.230Z" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "context": { 3 | "eventId": "1147091835525187", 4 | "timestamp": "2020-04-23T07:38:57.772Z", 5 | "eventType": "google.storage.object.finalize", 6 | "resource": { 7 | "service": "storage.googleapis.com", 8 | "name": "projects/_/buckets/some-bucket/objects/folder/Test.cs", 9 | "type": "storage#object" 10 | } 11 | }, 12 | "data": { 13 | "bucket": "some-bucket", 14 | "contentType": "text/plain", 15 | "crc32c": "rTVTeQ==", 16 | "etag": "CNHZkbuF/ugCEAE=", 17 | "generation": "1587627537231057", 18 | "id": "some-bucket/folder/Test.cs/1587627537231057", 19 | "kind": "storage#object", 20 | "md5Hash": "kF8MuJ5+CTJxvyhHS1xzRg==", 21 | "mediaLink": "https://www.googleapis.com/download/storage/v1/b/some-bucket/o/folder%2FTest.cs?generation=1587627537231057\u0026alt=media", 22 | "metageneration": "1", 23 | "name": "folder/Test.cs", 24 | "selfLink": "https://www.googleapis.com/storage/v1/b/some-bucket/o/folder/Test.cs", 25 | "size": "352", 26 | "storageClass": "MULTI_REGIONAL", 27 | "timeCreated": "2020-04-23T07:38:57.230Z", 28 | "timeStorageClassUpdated": "2020-04-23T07:38:57.230Z", 29 | "updated": "2020-04-23T07:38:57.230Z" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /invoker/core/src/test/resources/typed_nameconcat_request.json: -------------------------------------------------------------------------------- 1 | { 2 | "firstName": "John", 3 | "lastName": "Doe" 4 | } -------------------------------------------------------------------------------- /invoker/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.sonatype.oss 5 | oss-parent 6 | 9 7 | 8 | 9 | com.google.cloud.functions.invoker 10 | java-function-invoker-parent 11 | 1.4.1 12 | pom 13 | GCF Java Invoker Parent 14 | 15 | Parent POM for the GCF Java Invoker. The project is structured like this so 16 | that we can have modules that build jar files for use in tests. 17 | 18 | https://github.com/GoogleCloudPlatform/functions-framework-java/tree/main/invoker 19 | 20 | 21 | http://github.com/GoogleCloudPlatform/functions-framework-java 22 | scm:git:git://github.com/GoogleCloudPlatform/functions-framework-java.git 23 | scm:git:ssh://git@github.com/GoogleCloudPlatform/functions-framework-java.git 24 | HEAD 25 | 26 | 27 | 28 | core 29 | testfunction 30 | conformance 31 | 32 | 33 | 34 | UTF-8 35 | 3.8.1 36 | 11 37 | 11 38 | 39 | 40 | 41 | 42 | 43 | com.google.cloud.functions 44 | functions-framework-api 45 | 1.1.4 46 | 47 | 48 | 49 | 50 | 51 | 52 | sonatype-nexus-snapshots 53 | Sonatype Nexus Snapshots 54 | https://oss.sonatype.org/content/repositories/snapshots/ 55 | 56 | 57 | sonatype-nexus-staging 58 | Nexus Release Repository 59 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 60 | 61 | 62 | 63 | 64 | sonatype-oss-release 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-source-plugin 70 | 3.2.1 71 | 72 | 73 | attach-sources 74 | 75 | jar-no-fork 76 | 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-javadoc-plugin 83 | 3.11.2 84 | 85 | 86 | attach-javadocs 87 | 88 | jar 89 | 90 | 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-gpg-plugin 96 | 3.2.7 97 | 98 | 99 | sign-artifacts 100 | verify 101 | 102 | sign 103 | 104 | 105 | 106 | 107 | 108 | org.sonatype.plugins 109 | nexus-staging-maven-plugin 110 | 1.7.0 111 | true 112 | 113 | sonatype-nexus-snapshots 114 | https://oss.sonatype.org/ 115 | true 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /invoker/testfunction/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | 5 | com.google.cloud.functions.invoker 6 | java-function-invoker-parent 7 | 1.4.1 8 | 9 | 10 | com.google.cloud.functions.invoker 11 | java-function-invoker-testfunction 12 | 1.4.1 13 | Example GCF Function Jar 14 | 15 | An example of a GCF function packaged into a jar. We use this in tests. 16 | 17 | 18 | 19 | 20 | com.google.cloud.functions 21 | functions-framework-api 22 | 1.1.4 23 | 24 | 25 | 27 | com.google.escapevelocity 28 | escapevelocity 29 | 1.1 30 | 31 | 32 | com.google.guava 33 | guava 34 | 33.4.0-jre 35 | 36 | 37 | com.google.code.gson 38 | gson 39 | 2.12.1 40 | 41 | 42 | 43 | 44 | 45 | 46 | maven-jar-plugin 47 | 3.4.2 48 | 49 | 50 | 51 | true 52 | lib 53 | 54 | 55 | 56 | 57 | 58 | 59 | test-jar 60 | 61 | test-compile 62 | 63 | 64 | 65 | 71 | 72 | org.apache.maven.plugins 73 | maven-dependency-plugin 74 | 75 | 76 | test-compile 77 | 78 | copy-dependencies 79 | 80 | 81 | ${project.build.directory}/lib 82 | 83 | 84 | 85 | 86 | 87 | org.apache.maven.plugins 88 | maven-deploy-plugin 89 | 3.1.4 90 | 91 | true 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /invoker/testfunction/src/test/java/com/example/functionjar/Background.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.example.functionjar; 16 | 17 | import com.google.cloud.functions.Context; 18 | import com.google.cloud.functions.RawBackgroundFunction; 19 | import com.google.gson.Gson; 20 | import com.google.gson.JsonObject; 21 | import com.google.gson.JsonPrimitive; 22 | 23 | /** 24 | * @author emcmanus@google.com (Éamonn McManus) 25 | */ 26 | public class Background implements RawBackgroundFunction { 27 | @Override 28 | public void accept(String json, Context context) { 29 | try { 30 | test(json); 31 | } catch (Throwable e) { 32 | e.printStackTrace(); 33 | throw e; 34 | } 35 | } 36 | 37 | private void test(String jsonString) { 38 | Gson gson = new Gson(); 39 | JsonObject json = gson.fromJson(jsonString, JsonObject.class); 40 | JsonPrimitive jsonRuntimeClassName = json.getAsJsonPrimitive("class"); 41 | String runtimeClassName = jsonRuntimeClassName.getAsString(); 42 | new Checker().serviceOrAssert(runtimeClassName); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /invoker/testfunction/src/test/java/com/example/functionjar/Checker.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.example.functionjar; 16 | 17 | import com.google.escapevelocity.Template; 18 | 19 | class Checker { 20 | void serviceOrAssert(String runtimeClassName) { 21 | // Check that the context class loader is the loader that loaded this class. 22 | if (getClass().getClassLoader() != Thread.currentThread().getContextClassLoader()) { 23 | throw new AssertionError( 24 | String.format( 25 | "ClassLoader mismatch: mine %s; context %s", 26 | getClass().getClassLoader(), Thread.currentThread().getContextClassLoader())); 27 | } 28 | 29 | ClassLoader myLoader = getClass().getClassLoader(); 30 | Class