From bc06f7102047e852a4e3e5c53dce261b688aac8f Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Fri, 14 Mar 2025 09:37:51 +0100
Subject: [PATCH 01/32] Moved sources
---
.github/actions/build/action.yml | 39 +++
.github/actions/deploy-release/action.yml | 94 ++++++
.github/actions/deploy/action.yml | 62 ++++
.github/actions/newrelease/action.yml | 36 +++
.../actions/scan-with-blackduck/action.yaml | 54 ++++
.github/actions/scan-with-sonar/action.yaml | 48 +++
.github/dependabot.yml | 12 +
.github/workflows/codeql.yml | 92 ++++++
.../workflows/main-build-and-deploy-oss.yml | 104 +++++++
.github/workflows/main-build-and-deploy.yml | 96 ++++++
.github/workflows/main-build.yml | 96 ++++++
.github/workflows/pull-request-build.yml | 36 +++
.gitignore | 97 ++++++
.pipeline/config.yml | 40 +++
REUSE.toml | 11 +
.../maven/settings.xml | 9 +
cds-feature-advanced-event-mesh/pom.xml | 280 ++++++++++++++++++
.../aem/client/AemManagementClient.java | 150 ++++++++++
.../binding/AemAuthorizationServiceView.java | 72 +++++
.../aem/client/binding/AemEndpointView.java | 78 +++++
.../binding/AemOauth2PropertySupplier.java | 101 +++++++
.../client/binding/AemTokenFetchClient.java | 45 +++
.../jms/AemMessagingConnectionProvider.java | 120 ++++++++
.../aem/service/AemMessagingService.java | 114 +++++++
.../AemMessagingServiceConfiguration.java | 126 ++++++++
...s.services.runtime.CdsRuntimeConfiguration | 1 +
.../AemMessagingServiceConfigurationTest.java | 113 +++++++
.../src/test/resources/default-env.json | 33 +++
deploy-oss/pom.xml | 96 ++++++
pom.xml | 258 ++++++++++++++++
30 files changed, 2513 insertions(+)
create mode 100644 .github/actions/build/action.yml
create mode 100644 .github/actions/deploy-release/action.yml
create mode 100644 .github/actions/deploy/action.yml
create mode 100644 .github/actions/newrelease/action.yml
create mode 100644 .github/actions/scan-with-blackduck/action.yaml
create mode 100644 .github/actions/scan-with-sonar/action.yaml
create mode 100644 .github/dependabot.yml
create mode 100644 .github/workflows/codeql.yml
create mode 100644 .github/workflows/main-build-and-deploy-oss.yml
create mode 100644 .github/workflows/main-build-and-deploy.yml
create mode 100644 .github/workflows/main-build.yml
create mode 100644 .github/workflows/pull-request-build.yml
create mode 100644 .gitignore
create mode 100644 .pipeline/config.yml
create mode 100644 REUSE.toml
create mode 100644 cds-feature-advanced-event-mesh/maven/settings.xml
create mode 100644 cds-feature-advanced-event-mesh/pom.xml
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceView.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemOauth2PropertySupplier.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfiguration.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration
create mode 100644 cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/AemMessagingServiceConfigurationTest.java
create mode 100644 cds-feature-advanced-event-mesh/src/test/resources/default-env.json
create mode 100644 deploy-oss/pom.xml
create mode 100644 pom.xml
diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
new file mode 100644
index 0000000..5960381
--- /dev/null
+++ b/.github/actions/build/action.yml
@@ -0,0 +1,39 @@
+name: Maven Build
+description: "Builds a Maven project."
+
+inputs:
+ java-version:
+ description: "The Java version the build shall run with."
+ required: true
+ maven-version:
+ description: "The Maven version the build shall run with."
+ required: true
+ mutation-testing:
+ description: "Whether to run mutation testing."
+ default: 'true'
+ required: false
+
+runs:
+ using: composite
+ steps:
+ - name: Set up Java ${{ inputs.java-version }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ inputs.java-version }}
+ distribution: sapmachine
+ cache: maven
+
+ - name: Setup Maven ${{ inputs.maven-version }}
+ uses: stCarolas/setup-maven@v5
+ with:
+ maven-version: ${{ inputs.maven-version }}
+
+ - name: Piper Maven build
+ uses: SAP/project-piper-action@main
+ with:
+ step-name: mavenBuild
+
+ #- name: Mutation Testing
+ # if: ${{ inputs.mutation-testing == 'true' }}
+ # run: mvn org.pitest:pitest-maven:mutationCoverage -f cds-feature-advanced-event-mesh/pom.xml -ntp -B
+ # shell: bash
diff --git a/.github/actions/deploy-release/action.yml b/.github/actions/deploy-release/action.yml
new file mode 100644
index 0000000..fa48dee
--- /dev/null
+++ b/.github/actions/deploy-release/action.yml
@@ -0,0 +1,94 @@
+name: Deploy Release to Maven Central
+description: "Deploys released artifacts to Maven Central repository."
+
+inputs:
+ user:
+ description: "The user used for the upload (technical user for maven central upload)"
+ required: true
+ password:
+ description: "The password used for the upload (technical user for maven central upload)"
+ required: true
+ profile:
+ description: "The profile id"
+ required: true
+ pgp-pub-key:
+ description: "The public pgp key ID"
+ required: true
+ pgp-private-key:
+ description: "The private pgp key"
+ required: true
+ pgp-passphrase:
+ description: "The passphrase for pgp"
+ required: true
+ revision:
+ description: "The revision of cds-feature-advanced-event-mesh"
+ required: true
+ maven-version:
+ description: "The Maven version the build shall run with."
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Echo Inputs
+ run: |
+ echo "user: ${{ inputs.user }}"
+ echo "profile: ${{ inputs.profile }}"
+ echo "revision: ${{ inputs.revision }}"
+ shell: bash
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: sapmachine
+ java-version: '17'
+ cache: maven
+ server-id: ossrh
+ server-username: MAVEN_CENTRAL_USER
+ server-password: MAVEN_CENTRAL_PASSWORD
+
+ - name: Set up Maven ${{ inputs.maven-version }}
+ uses: stCarolas/setup-maven@v5
+ with:
+ maven-version: ${{ inputs.maven-version }}
+
+ - name: Import GPG Key
+ run: |
+ echo "${{ inputs.pgp-private-key }}" | gpg --batch --passphrase "$PASSPHRASE" --import
+ shell: bash
+ env:
+ PASSPHRASE: ${{ inputs.pgp-passphrase }}
+
+ - name: Deploy Locally
+ run: >
+ mvn -B -ntp -fae --show-version
+ -Durl=file:./temp_local_repo
+ -Dmaven.install.skip=true
+ -Dmaven.test.skip=true
+ -Dgpg.passphrase="$GPG_PASSPHRASE"
+ -Dgpg.keyname="$GPG_PUB_KEY"
+ -Drevision="${{ inputs.revision }}"
+ deploy
+ working-directory: ./deploy-oss
+ shell: bash
+ env:
+ MAVEN_CENTRAL_USER: ${{ inputs.user }}
+ MAVEN_CENTRAL_PASSWORD: ${{ inputs.password }}
+ GPG_PASSPHRASE: ${{ inputs.pgp-passphrase }}
+ GPG_PUB_KEY: ${{ inputs.pgp-pub-key }}
+
+ - name: Deploy Staging
+ run: >
+ mvn -B -ntp -fae --show-version
+ org.sonatype.plugins:nexus-staging-maven-plugin:1.6.13:deploy-staged-repository
+ -DserverId=ossrh
+ -DnexusUrl=https://oss.sonatype.org
+ -DrepositoryDirectory=./temp_local_repo
+ -DstagingProfileId="$MAVEN_CENTRAL_PROFILE_ID"
+ -Drevision="${{ inputs.revision }}"
+ working-directory: ./deploy-oss
+ shell: bash
+ env:
+ MAVEN_CENTRAL_USER: ${{ inputs.user }}
+ MAVEN_CENTRAL_PASSWORD: ${{ inputs.password }}
+ MAVEN_CENTRAL_PROFILE_ID: ${{ inputs.profile }}
diff --git a/.github/actions/deploy/action.yml b/.github/actions/deploy/action.yml
new file mode 100644
index 0000000..fd3e1cd
--- /dev/null
+++ b/.github/actions/deploy/action.yml
@@ -0,0 +1,62 @@
+name: Deploy to artifactory
+description: "Deploys artifacts to artifactory."
+
+inputs:
+ repository-url:
+ description: "The URL of the repository to upload to."
+ required: true
+ server-id:
+ description: "The service id of the repository to upload to."
+ required: true
+ user:
+ description: "The user used for the upload."
+ required: true
+ password:
+ description: "The password used for the upload."
+ required: true
+ pom-file:
+ description: "The path to the POM file."
+ required: false
+ default: "pom.xml"
+ maven-version:
+ description: "The Maven version the build shall run with."
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Echo Inputs
+ run: |
+ echo "repository-url: ${{ inputs.repository-url }}"
+ echo "user: ${{ inputs.user }}"
+ echo "password: ${{ inputs.password }}"
+ echo "pom-file: ${{ inputs.pom-file }}"
+ echo "altDeploymentRepository: ${{inputs.server-id}}::${{inputs.repository-url}}"
+ shell: bash
+
+ - name: Setup Java 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: sapmachine
+ java-version: '17'
+ server-id: ${{ inputs.server-id }}
+ server-username: DEPLOYMENT_USER
+ server-password: DEPLOYMENT_PASS
+
+ - name: Setup Maven ${{ inputs.maven-version }}
+ uses: stCarolas/setup-maven@v5
+ with:
+ maven-version: ${{ inputs.maven-version }}
+
+ - name: Deploy
+ run: >
+ mvn -B -ntp -fae --show-version
+ -DaltDeploymentRepository=${{inputs.server-id}}::${{inputs.repository-url}}
+ -Dmaven.install.skip=true
+ -Dmaven.test.skip=true
+ -f ${{ inputs.pom-file }}
+ deploy
+ env:
+ DEPLOYMENT_USER: ${{ inputs.user }}
+ DEPLOYMENT_PASS: ${{ inputs.password }}
+ shell: bash
diff --git a/.github/actions/newrelease/action.yml b/.github/actions/newrelease/action.yml
new file mode 100644
index 0000000..c7b7113
--- /dev/null
+++ b/.github/actions/newrelease/action.yml
@@ -0,0 +1,36 @@
+name: Update POM with new release
+description: Updates the revision property in the POM file with the new release version.
+
+inputs:
+ java-version:
+ description: "The Java version the build shall run with."
+ required: true
+ maven-version:
+ description: "The Maven version the build shall run with."
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Set up Java ${{ inputs.java-version }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ inputs.java-version }}
+ distribution: sapmachine
+ cache: maven
+
+ - name: Setup Maven ${{ inputs.maven-version }}
+ uses: stCarolas/setup-maven@v5
+ with:
+ maven-version: ${{ inputs.maven-version }}
+
+ - name: Update version
+ run: |
+ VERSION=$(echo "${{ github.ref }}" | sed -e 's,.*/\(.*\),\1,')
+ mvn --no-transfer-progress versions:set-property -Dproperty=revision -DnewVersion=$VERSION
+ git config --global user.name 'github-actions[bot]'
+ git config --global user.email 'github-actions[bot]@users.noreply.github.com'
+ git checkout -b main
+ git commit -am "Update version to $VERSION"
+ git push --set-upstream origin main
+ shell: bash
\ No newline at end of file
diff --git a/.github/actions/scan-with-blackduck/action.yaml b/.github/actions/scan-with-blackduck/action.yaml
new file mode 100644
index 0000000..76be691
--- /dev/null
+++ b/.github/actions/scan-with-blackduck/action.yaml
@@ -0,0 +1,54 @@
+name: "Scan with BlackDuck"
+description: "Scans the project with BlackDuck"
+
+inputs:
+ blackduck_token:
+ description: "The token to use for BlackDuck authentication"
+ required: true
+ github_token:
+ description: "The token to use for GitHub authentication"
+ required: true
+ java-version:
+ description: "The version of Java to use"
+ default: '17'
+ required: false
+ maven-version:
+ description: "The Maven version the build shall run with."
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Set up Java ${{ inputs.java-version }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ inputs.java-version }}
+ distribution: sapmachine
+ cache: maven
+
+ - name: Setup Maven ${{ inputs.maven-version }}
+ uses: stCarolas/setup-maven@v5
+ with:
+ maven-version: ${{ inputs.maven-version }}
+
+ - name: Get Major Version
+ id: get-major-version
+ run: |
+ echo "REVISION=$(mvn help:evaluate -Dexpression=revision -q -DforceStdout)" >> $GITHUB_OUTPUT
+ shell: bash
+
+ - name: Print Version Number
+ run: echo "${{ steps.get-major-version.outputs.REVISION }}"
+ shell: bash
+
+ - name: BlackDuck Scan
+ uses: SAP/project-piper-action@main
+ with:
+ step-name: detectExecuteScan
+ flags: \
+ --githubToken=$GITHUB_token \
+ --version=${{ steps.get-major-version.outputs.REVISION }}
+ env:
+ PIPER_token: ${{ inputs.blackduck_token }}
+ GITHUB_token: ${{ inputs.github_token }}
+ SCAN_MODE: FULL
diff --git a/.github/actions/scan-with-sonar/action.yaml b/.github/actions/scan-with-sonar/action.yaml
new file mode 100644
index 0000000..34522cf
--- /dev/null
+++ b/.github/actions/scan-with-sonar/action.yaml
@@ -0,0 +1,48 @@
+name: Scan with SonarQube
+description: Scans the project with SonarQube
+
+inputs:
+ sonarq-token:
+ description: The token to use for SonarQube authentication
+ required: true
+ github-token:
+ description: The token to use for GitHub authentication
+ required: true
+ java-version:
+ description: The version of Java to use
+ required: true
+ maven-version:
+ description: The version of Maven to use
+ required: true
+
+runs:
+ using: composite
+
+ steps:
+ - name: Set up Java ${{inputs.java-version}}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{inputs.java-version}}
+ distribution: sapmachine
+ cache: maven
+
+ - name: Set up Maven ${{inputs.maven-version}}
+ uses: stCarolas/setup-maven@v5
+ with:
+ maven-version: ${{inputs.maven-version}}
+
+ - name: Get Revision
+ id: get-revision
+ run: |
+ echo "REVISION=$(mvn help:evaluate -Dexpression=revision -q -DforceStdout)" >> $GITHUB_OUTPUT
+ shell: bash
+
+ - name: Print Revision
+ run: echo "${{steps.get-revision.outputs.REVISION}}"
+ shell: bash
+
+ - name: SonarQube Scan
+ uses: SAP/project-piper-action@main
+ with:
+ step-name: sonarExecuteScan
+ flags: --token=${{inputs.sonarq-token}} --githubToken=${{inputs.github-token}} --version=${{steps.get-revision.outputs.REVISION}} --inferJavaBinaries=true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..50390b2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,12 @@
+version: 2
+updates:
+ - package-ecosystem: maven
+ directories:
+ - "/**/*"
+ schedule:
+ interval: daily
+ open-pull-requests-limit: 10
+ - package-ecosystem: github-actions
+ directory: "/"
+ schedule:
+ interval: daily
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..4337ef6
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,92 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL Advanced"
+
+on:
+ push:
+ branches: [ "main" ]
+ pull_request:
+ branches: [ "main" ]
+ schedule:
+ - cron: '24 18 * * 2'
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ # Runner size impacts CodeQL analysis time. To learn more, please see:
+ # - https://gh.io/recommended-hardware-resources-for-running-codeql
+ # - https://gh.io/supported-runners-and-hardware-resources
+ # - https://gh.io/using-larger-runners (GitHub.com only)
+ # Consider using larger runners or machines with greater resources for possible analysis time improvements.
+ runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
+ permissions:
+ # required for all workflows
+ security-events: write
+
+ # required to fetch internal or private CodeQL packs
+ packages: read
+
+ # only required for workflows in private repositories
+ actions: read
+ contents: read
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - language: java-kotlin
+ build-mode: none # This mode only analyzes Java. Set this to 'autobuild' or 'manual' to analyze Kotlin too.
+ # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
+ # Use `c-cpp` to analyze code written in C, C++ or both
+ # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
+ # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
+ # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
+ # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
+ # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
+ # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ build-mode: ${{ matrix.build-mode }}
+ # If you wish to specify custom queries, you can do so here or in a config file.
+ # By default, queries listed here will override any specified in a config file.
+ # Prefix the list here with "+" to use these queries and those in the config file.
+
+ # For more 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
+ # queries: security-extended,security-and-quality
+
+ # If the analyze step fails for one of the languages you are analyzing with
+ # "We were unable to automatically build your code", modify the matrix above
+ # to set the build mode to "manual" for that language. Then modify this step
+ # to build your code.
+ # âšī¸ Command-line programs to run using the OS shell.
+ # đ See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
+ - if: matrix.build-mode == 'manual'
+ shell: bash
+ run: |
+ echo 'If you are using a "manual" build mode for one or more of the' \
+ 'languages you are analyzing, replace this with the commands to build' \
+ 'your code, for example:'
+ echo ' make bootstrap'
+ echo ' make release'
+ exit 1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{matrix.language}}"
diff --git a/.github/workflows/main-build-and-deploy-oss.yml b/.github/workflows/main-build-and-deploy-oss.yml
new file mode 100644
index 0000000..c7d91e1
--- /dev/null
+++ b/.github/workflows/main-build-and-deploy-oss.yml
@@ -0,0 +1,104 @@
+name: Deploy to OSS
+
+env:
+ JAVA_VERSION: '17'
+ MAVEN_VERSION: '3.6.3'
+
+on:
+ release:
+ types: [ "released" ]
+
+jobs:
+
+ # blackduck:
+ # name: "Blackduck Scan"
+ # runs-on: ubuntu-latest
+ # timeout-minutes: 15
+ # steps:
+ # - name: Checkout
+ # uses: actions/checkout@v4
+ # - name: "Scan With Black Duck"
+ # uses: ./.github/actions/scan-with-blackduck
+ # with:
+ # blackduck_token: ${{ secrets.BLACK_DUCK_TOKEN }}
+ # github_token: ${{ secrets.GITHUB_TOKEN }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+
+ update-version:
+ runs-on: ubuntu-latest
+ # needs: blackduck
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ #with:
+ # token: ${{ secrets.GH_TOKEN }}
+
+ #- name: Update version
+ # uses: ./.github/actions/newrelease
+ # with:
+ # java-version: ${{ env.JAVA_VERSION }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+
+ - name: Upload Changed Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: root-new-version
+ path: .
+ include-hidden-files: true
+ retention-days: 1
+
+ build:
+ runs-on: ubuntu-latest
+ needs: update-version
+ steps:
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: root-new-version
+
+ - name: Build
+ uses: ./.github/actions/build
+ with:
+ java-version: ${{ env.JAVA_VERSION }}
+ maven-version: ${{ env.MAVEN_VERSION }}
+
+ #- name: SonarQube Scan
+ # uses: ./.github/actions/scan-with-sonar
+ # with:
+ # java-version: ${{ env.JAVA_VERSION }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+ # sonarq-token: ${{ secrets.SONARQ_TOKEN }}
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Upload Changed Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: root-build
+ include-hidden-files: true
+ path: .
+ retention-days: 1
+
+ deploy:
+ name: Deploy to Maven Central
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: root-build
+
+ - name: Deploy
+ uses: ./.github/actions/deploy-release
+ with:
+ user: ${{ secrets.OSSRH_SONATYPE_ORG_API_USER }}
+ password: ${{ secrets.OSSRH_SONATYPE_ORG_API_PASSWD }}
+ profile: ${{ secrets.OSSRH_SONATYPE_ORG_PROFILE_ID }}
+ pgp-pub-key: ${{ secrets.PGP_PUBKEY_ID }}
+ pgp-private-key: ${{ secrets.PGP_PRIVATE_KEY }}
+ pgp-passphrase: ${{ secrets.PGP_PASSPHRASE }}
+ revision: ${{ github.event.release.tag_name }}
+ maven-version: ${{ env.MAVEN_VERSION }}
+
+ - name: Echo Status
+ run: echo "The job status is ${{ job.status }}"
diff --git a/.github/workflows/main-build-and-deploy.yml b/.github/workflows/main-build-and-deploy.yml
new file mode 100644
index 0000000..5f10872
--- /dev/null
+++ b/.github/workflows/main-build-and-deploy.yml
@@ -0,0 +1,96 @@
+name: Deploy to Artifactory
+
+env:
+ JAVA_VERSION: '17'
+ MAVEN_VERSION: '3.6.3'
+ DEPLOY_REPOSITORY_URL: 'https://common.repositories.cloud.sap/artifactory/cap-java'
+ POM_FILE: '.flattened-pom.xml'
+
+on:
+ release:
+ types: [ "prereleased" ]
+
+jobs:
+
+ # blackduck:
+ # name: Blackduck Scan
+ # runs-on: ubuntu-latest
+ # timeout-minutes: 15
+ # steps:
+ # - name: Checkout
+ # uses: actions/checkout@v4
+ # - name: Scan With Black Duck
+ # uses: ./.github/actions/scan-with-blackduck
+ # with:
+ # blackduck_token: ${{ secrets.BLACK_DUCK_TOKEN }}
+ # github_token: ${{ secrets.GITHUB_TOKEN }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+
+ update-version:
+ runs-on: ubuntu-latest
+ # needs: blackduck
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ #with:
+ # token: ${{ secrets.GH_TOKEN }}
+
+ #- name: Update version
+ # uses: ./.github/actions/newrelease
+ # with:
+ # java-version: ${{ env.JAVA_VERSION }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+
+ - name: Upload Changed Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: root-new-version
+ include-hidden-files: true
+ path: .
+ retention-days: 1
+
+ build:
+ runs-on: ubuntu-latest
+ needs: update-version
+ steps:
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: root-new-version
+
+ - name: Build
+ uses: ./.github/actions/build
+ with:
+ java-version: ${{ env.JAVA_VERSION }}
+ maven-version: ${{ env.MAVEN_VERSION }}
+
+ - name: Upload Changed Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: root-build
+ include-hidden-files: true
+ path: .
+ retention-days: 1
+
+ deploy:
+ name: Deploy to Artifactory
+ runs-on: ubuntu-latest
+ needs: build
+ steps:
+ - name: Download artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: root-build
+
+ - name: Deploy with Maven
+ uses: ./.github/actions/deploy
+ with:
+ user: ${{ secrets.DEPLOYMENT_USER }}
+ password: ${{ secrets.DEPLOYMENT_PASS }}
+ server-id: artifactory
+ repository-url: ${{ env.DEPLOY_REPOSITORY_URL }}
+ pom-file: ${{ env.POM_FILE }}
+ maven-version: ${{ env.MAVEN_VERSION }}
+
+ - name: Echo Status
+ run: echo "The job status is ${{ job.status }}"
diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml
new file mode 100644
index 0000000..c4c8162
--- /dev/null
+++ b/.github/workflows/main-build.yml
@@ -0,0 +1,96 @@
+name: Main build and deploy
+
+env:
+ JAVA_VERSION: '17'
+ MAVEN_VERSION: '3.6.3'
+
+on:
+ push:
+ branches: [ "main" ]
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ java-version: [ 17, 21 ]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Build
+ uses: ./.github/actions/build
+ with:
+ java-version: ${{ matrix.java-version }}
+ maven-version: ${{ env.MAVEN_VERSION }}
+
+ #- name: SonarQube Scan
+ # uses: ./.github/actions/scan-with-sonar
+ # if: ${{ matrix.java-version == 17 }}
+ # with:
+ # java-version: ${{ matrix.java-version }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+ # sonarq-token: ${{ secrets.SONARQ_TOKEN }}
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
+
+ # scan:
+ # name: Blackduck Scan
+ # runs-on: ubuntu-latest
+ # timeout-minutes: 15
+ # steps:
+ # - name: Checkout
+ # uses: actions/checkout@v4
+ # - name: Scan
+ # uses: ./.github/actions/scan-with-blackduck
+ # with:
+ # blackduck_token: ${{ secrets.BLACK_DUCK_TOKEN }}
+ # github_token: ${{ secrets.GITHUB_TOKEN }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+
+ deploy-snapshot:
+ name: Deploy snapshot to Artifactory
+ runs-on: ubuntu-latest
+ needs: [build] # [build, scan]
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Java ${{ env.JAVA_VERSION }}
+ uses: actions/setup-java@v4
+ with:
+ java-version: ${{ env.JAVA_VERSION }}
+ distribution: sapmachine
+ cache: maven
+ server-id: artifactory
+ server-username: DEPLOYMENT_USER
+ server-password: DEPLOYMENT_PASS
+
+ - name: Set up Maven ${{ env.MAVEN_VERSION }}
+ uses: stCarolas/setup-maven@v5
+ with:
+ maven-version: ${{ env.MAVEN_VERSION }}
+
+ - name: Get Revision
+ id: get-revision
+ run: |
+ echo "REVISION=$(mvn help:evaluate -Dexpression=revision -q -DforceStdout)" >> $GITHUB_OUTPUT
+ shell: bash
+
+ - name: Print Revision
+ run: echo "Current revision ${{ steps.get-revision.outputs.REVISION }}"
+ shell: bash
+
+ - name: Deploy snapshot
+ if: ${{ endsWith(steps.get-revision.outputs.REVISION, '-SNAPSHOT') }}
+ # https://maven.apache.org/plugins/maven-deploy-plugin/usage.html#the-deploy-deploy-mojo
+ run: >
+ mvn -B -ntp -fae
+ -Dmaven.install.skip=true
+ -Dmaven.test.skip=true
+ -DdeployAtEnd=true
+ deploy
+ env:
+ DEPLOYMENT_USER: ${{ secrets.DEPLOYMENT_USER }}
+ DEPLOYMENT_PASS: ${{ secrets.DEPLOYMENT_PASS }}
+ shell: bash
diff --git a/.github/workflows/pull-request-build.yml b/.github/workflows/pull-request-build.yml
new file mode 100644
index 0000000..21f8beb
--- /dev/null
+++ b/.github/workflows/pull-request-build.yml
@@ -0,0 +1,36 @@
+name: Pull Request Voter
+
+env:
+ MAVEN_VERSION: '3.6.3'
+
+on:
+ pull_request:
+ branches: [ "main" ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ java-version: [ 17, 21 ]
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Build
+ uses: ./.github/actions/build
+ with:
+ java-version: ${{ matrix.java-version }}
+ maven-version: ${{ env.MAVEN_VERSION }}
+
+ #- name: SonarQube Scan
+ # uses: ./.github/actions/scan-with-sonar
+ # if: ${{ matrix.java-version == 17 }}
+ # with:
+ # java-version: ${{ matrix.java-version }}
+ # maven-version: ${{ env.MAVEN_VERSION }}
+ # sonarq-token: ${{ secrets.SONARQ_TOKEN }}
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e27d89b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,97 @@
+## IntelliJ
+.idea
+*.iml
+
+## Eclipse
+.project
+.classpath
+.settings/
+
+## Java
+target/
+
+.flattened-pom.xml
+node_modules
+
+## PMD
+.pmd
+.pmdruleset.xml
+
+## files required for local execution of github actions with act
+.env
+.secrets
+event.json
+
+### Linux ###
+*~
+
+# temporary files which can be created if a process still has a handle open of a deleted file
+.fuse_hidden*
+
+# KDE directory preferences
+.directory
+
+# Linux trash folder which might appear on any partition or disk
+.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
diff --git a/.pipeline/config.yml b/.pipeline/config.yml
new file mode 100644
index 0000000..4a2325d
--- /dev/null
+++ b/.pipeline/config.yml
@@ -0,0 +1,40 @@
+steps:
+ mavenBuild:
+ verbose: false
+ verify: true
+ flatten: true
+ # https://www.project-piper.io/steps/mavenBuild/#dockerimage
+ # If empty, Docker is not used and the command is executed directly on the Jenkins system.
+ dockerImage: ''
+
+ detectExecuteScan:
+ projectName: 'com.sap.cds.feature.advanced-event-mesh'
+ groups:
+ - 'CDSJAVA-OPEN-SOURCE'
+ serverUrl: 'https://sap.blackducksoftware.com/'
+ mavenExcludedScopes: [ "provided", "test" ]
+ failOn: [ 'BLOCKER', 'CRITICAL', 'MAJOR' ]
+ versioningModel: "major-minor"
+ detectTools: [ 'DETECTOR', 'BINARY_SCAN' ]
+ installArtifacts: true
+ repository: '/cap-java/advanced-event-mesh'
+ verbose: true
+ scanProperties:
+ - --detect.included.detector.types=MAVEN
+ - --detect.excluded.directories='**/node_modules,**/*test*,**/localrepo,**/target/site,**/*-site.jar'
+ - --detect.maven.build.command='-pl com.sap.cds:cds-feature-advanced-event-mesh'
+ # https://www.project-piper.io/steps/detectExecuteScan/#dockerimage
+ # If empty, Docker is not used and the command is executed directly on the Jenkins system.
+ dockerImage: ''
+
+ sonarExecuteScan:
+ serverUrl: https://sonar.tools.sap
+ projectKey: cds-feature-advanced-event-mesh
+ # https://www.project-piper.io/steps/sonarExecuteScan/#dockerimage
+ # If empty, Docker is not used and the command is executed directly on the Jenkins system.
+ dockerImage: ''
+ options:
+ - sonar.qualitygate.wait=true
+ - sonar.java.source=17
+ - sonar.exclusions=**/node_modules/**,**/target/**
+ - sonar.coverage.jacoco.xmlReportPaths=cds-feature-advanced-event-mesh/target/site/jacoco/jacoco.xml
diff --git a/REUSE.toml b/REUSE.toml
new file mode 100644
index 0000000..bd9f73b
--- /dev/null
+++ b/REUSE.toml
@@ -0,0 +1,11 @@
+version = 1
+SPDX-PackageName = "cds-feature-event-hub"
+SPDX-PackageSupplier = "ospo@sap.com"
+SPDX-PackageDownloadLocation = "https://github.com/cap-java/cds-feature-event-hub"
+SPDX-PackageComment = "The code in this project may include calls to APIs (\"API Calls\") of\n SAP or third-party products or services developed outside of this project\n (\"External Products\").\n \"APIs\" means application programming interfaces, as well as their respective\n specifications and implementing code that allows software to communicate with\n other software.\n API Calls to External Products are not licensed under the open source license\n that governs this project. The use of such API Calls and related External\n Products are subject to applicable additional agreements with the relevant\n provider of the External Products. In no event shall the open source license\n that governs this project grant any rights in or to any External Products,or\n alter, expand or supersede any terms of the applicable additional agreements.\n If you have a valid license agreement with SAP for the use of a particular SAP\n External Product, then you may make use of any API Calls included in this\n project's code for that SAP External Product, subject to the terms of such\n license agreement. If you do not have a valid license agreement for the use of\n a particular SAP External Product, then you may only make use of any API Calls\n in this project for that SAP External Product for your internal, non-productive\n and non-commercial test and evaluation of such API Calls. Nothing herein grants\n you any rights to use or access any SAP External Product, or provide any third\n parties the right to use of access any SAP External Product, through API Calls."
+
+[[annotations]]
+path = "**"
+precedence = "aggregate"
+SPDX-FileCopyrightText = "2025 SAP SE or an SAP affiliate company and cds-feature-event-hub contributors"
+SPDX-License-Identifier = "Apache-2.0"
diff --git a/cds-feature-advanced-event-mesh/maven/settings.xml b/cds-feature-advanced-event-mesh/maven/settings.xml
new file mode 100644
index 0000000..30417a0
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/maven/settings.xml
@@ -0,0 +1,9 @@
+
+
+
+ artifactory
+ ${env.SERVER_USER}
+ ${env.SERVER_PASSWORD}
+
+
+
\ No newline at end of file
diff --git a/cds-feature-advanced-event-mesh/pom.xml b/cds-feature-advanced-event-mesh/pom.xml
new file mode 100644
index 0000000..2dc881e
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/pom.xml
@@ -0,0 +1,280 @@
+
+ 4.0.0
+
+
+ com.sap.cds
+ cds-feature-advanced-event-mesh-root
+ ${revision}
+
+
+ cds-feature-advanced-event-mesh
+ jar
+
+ CDS plugin for SAP Integration Suite, advanced event mesh
+ https://cap.cloud.sap/docs/plugins/#event-broker-plugin
+
+
+
+
+ com.sap.cds
+ cds-services-messaging
+
+
+
+ com.sap.cds
+ cds-integration-cloud-sdk
+
+
+
+ com.sap.cds
+ cds-services-utils
+
+
+
+ org.apache.qpid
+ qpid-jms-client
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+
+
+ com.sap.cds
+ cds-services-impl
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.0
+ test
+
+
+
+ org.slf4j
+ slf4j-reload4j
+ 2.0.17
+ test
+
+
+
+ com.jayway.jsonpath
+ json-path
+ 2.9.0
+ test
+
+
+
+ org.mock-server
+ mockserver-netty
+ test
+ 5.15.0
+
+
+ com.google.guava
+ guava
+
+
+ commons-io
+ commons-io
+
+
+
+
+
+
+ ${project.artifactId}
+
+
+
+ org.pitest
+ pitest-maven
+
+
+ com.sap.cds.feature.messaging.aem.**
+
+
+ CONSTRUCTOR_CALLS
+ VOID_METHOD_CALLS
+ NON_VOID_METHOD_CALLS
+ REMOVE_CONDITIONALS_ORDER_ELSE
+ CONDITIONALS_BOUNDARY
+ EMPTY_RETURNS
+ NEGATE_CONDITIONALS
+ REMOVE_CONDITIONALS_EQUAL_IF
+ REMOVE_CONDITIONALS_EQUAL_ELSE
+ REMOVE_CONDITIONALS_ORDER_IF
+ REMOVE_CONDITIONALS_ORDER_ELSE
+
+ 95
+ 90
+
+
+
+
+ org.pitest
+ pitest-junit5-plugin
+ 1.2.2
+
+
+
+
+
+ maven-clean-plugin
+
+
+
+ src/test/resources
+
+ schema.sql
+
+
+
+ src/test/resources/cds
+
+ csn.json
+
+
+
+ src/test/resources/gen
+
+ **/*
+
+ false
+
+
+ ./
+
+ .flattened-pom.xml
+
+
+
+
+
+
+ auto-clean
+ clean
+
+ clean
+
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+
+
+ ${excluded.generation.package}**/*
+
+
+
+
+
+ jacoco-initialize
+
+ prepare-agent
+
+
+
+ jacoco-site-report-all-tests
+ verify
+
+ report
+
+
+
+ jacoco-site-report-only-unit-tests
+ test
+
+ report
+
+
+
+ jacoco-check-unit-tests-only
+ test
+
+ check
+
+
+
+
+ BUNDLE
+
+
+ INSTRUCTION
+ COVEREDRATIO
+ 0.5
+
+
+ BRANCH
+ COVEREDRATIO
+ 0.5
+
+
+ COMPLEXITY
+ COVEREDRATIO
+ 0.5
+
+
+ CLASS
+ MISSEDCOUNT
+ 3
+
+
+
+
+
+
+
+
+
+
+ maven-javadoc-plugin
+
+ ${skipDuringDeploy}
+ true
+ all,-missing
+
+
+
+
+ jar
+
+
+
+
+
+
+ maven-source-plugin
+
+
+
+ jar
+
+
+
+
+
+
+
+
+
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
new file mode 100644
index 0000000..b1172ab
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
@@ -0,0 +1,150 @@
+package com.sap.cds.feature.messaging.aem.client;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.http.HttpStatus;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.sap.cds.integration.cloudsdk.rest.client.JsonRestClient;
+import com.sap.cds.integration.cloudsdk.rest.client.JsonRestClientResponseException;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+
+public class AemManagementClient extends JsonRestClient {
+ private static final Logger logger = LoggerFactory.getLogger(AemManagementClient.class);
+
+ private static final String API_BASE = "/msgVpns/%s";
+ private static final String API_QUEUE = API_BASE + "/queues";
+ private static final String API_QUEUE_NAME = API_BASE + "/queues/%s";
+ private static final String API_QUEUE_NAME_SUBSCRIPTION = API_BASE + "/queues/%s/subscriptions";
+
+ public static final String ATTR_EGRESS_ENABLED = "egressEnabled";
+ public static final String ATTR_INGRESS_ENABLED = "ingressEnabled";
+ public static final String ATTR_DEAD_MSG_QUEUE = "deadMsgQueue";
+ public static final String ATTR_OWNER = "owner";
+ public static final String ATTR_QUEUE_NAME = "queueName";
+ public static final String ATTR_SUBSCRIPTION_TOPIC = "subscriptionTopic";
+
+ private final ServiceBinding binding;
+ private final String vpn;
+ private final String owner; // TODO Remove?
+
+ public AemManagementClient(ServiceBinding binding) {
+ super(ServiceBindingDestinationOptions.forService(binding).build());
+ this.binding = binding;
+ this.vpn = getVpn();
+ //this.owner = getOwner();
+ this.owner = this.vpn;
+ }
+
+ public void removeQueue(String queue) throws IOException {
+ logger.debug("Removing queue {}", queue);
+
+ deleteRequest(uri(API_QUEUE_NAME, this.vpn, queue));
+
+ logger.debug("Successfully removed queue {}", queue);
+ }
+
+ public JsonNode getQueue(String name) throws IOException {
+ try {
+ logger.debug("Retrieving information for queue {}", name);
+
+ JsonNode result = getRequest(uri(API_QUEUE_NAME, this.vpn, name));
+
+ logger.debug("Successfully retrieved information for queue {}: {}", name, result.asText());
+
+ return result;
+ } catch (JsonRestClientResponseException e) {
+ if (e.getResponseCode() == HttpStatus.SC_NOT_FOUND) {
+ logger.debug("Queue {} not found", name);
+ return null;
+ }
+
+ logger.error("Failed to retrieve information for queue {}", name, e);
+ return null;
+ }
+ }
+
+ public void createQueue(String name, Map properties) throws IOException {
+ // We have to read the queue first to check if it exists; only create it if it doesn't
+ logger.debug("Checking if queue {} exists", name);
+ JsonNode queue = getQueue(name);
+
+ if (queue == null) {
+ logger.debug("Queue {} does not exist, creating it", name);
+
+ Map attributes = new HashMap<>(properties);
+ attributes.put(ATTR_QUEUE_NAME, name);
+ attributes.put(ATTR_OWNER, this.owner);
+ attributes.put(ATTR_INGRESS_ENABLED, true);
+ attributes.put(ATTR_EGRESS_ENABLED, true);
+
+ ObjectNode data = mapper.convertValue(attributes, ObjectNode.class);
+ postRequest(uri(API_QUEUE, this.vpn), data);
+ }
+ }
+
+ public JsonNode getQueueSubscription(String queue) throws IOException {
+ logger.debug("Retrieving information for queue subscription {}", queue);
+ JsonNode result = getRequest(uri(API_QUEUE_NAME_SUBSCRIPTION, this.vpn, queue));
+
+ return result;
+ }
+
+ public void createQueueSubscription(String queue, String topic) throws IOException {
+ logger.debug("Checking if queue {} is subscribed to topic {}", queue, topic);
+
+ if (!isTopicSubscribed(getQueueSubscription(queue), queue, topic)) {
+ logger.debug("Queue {} is not subscribed to topic {}, subscribing it", queue, topic);
+
+ Map attributes = Map.of(ATTR_SUBSCRIPTION_TOPIC, topic);
+ ObjectNode data = mapper.convertValue(attributes, ObjectNode.class);
+ postRequest(uri(API_QUEUE_NAME_SUBSCRIPTION, this.vpn, queue), data);
+ }
+ }
+
+ private String uri(String path, Object... args) {
+ return String.format(
+ path,
+ (Object[]) Arrays.stream(args).map(Object::toString).map(this::urlEncode).toArray(String[]::new));
+ }
+
+ private String urlEncode(String value) {
+ return URLEncoder.encode(value, StandardCharsets.UTF_8);
+ }
+
+ public boolean isTopicSubscribed(JsonNode jsonNode, String queueName, String topic) {
+ String rawTopic = topic.replace("topic://", "");
+ if (jsonNode.has("data")) {
+ for (JsonNode dataNode : jsonNode.get("data")) {
+ if (dataNode.has("msgVpnName") && dataNode.has("queueName") && dataNode.has("subscriptionTopic")) {
+ String nodeMsgVpnName = dataNode.get("msgVpnName").asText();
+ String nodeQueueName = dataNode.get("queueName").asText();
+ String nodeSubscriptionTopic = dataNode.get("subscriptionTopic").asText();
+
+ if (this.getVpn().equals(nodeMsgVpnName) && queueName.equals(nodeQueueName) && rawTopic.equals(nodeSubscriptionTopic)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private String getVpn() {
+ return (String) this.binding.getCredentials().get("vpn");
+ }
+
+ private String getOwner() {
+ return (String) this.binding.getCredentials().get("user");
+ }
+
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceView.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceView.java
new file mode 100644
index 0000000..bc4358b
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceView.java
@@ -0,0 +1,72 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import java.util.Map;
+import java.util.Optional;
+
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+
+/**
+ * AemAuthorizationServiceView provides a view of the authorization service
+ * credentials from a given ServiceBinding.
+ */
+public class AemAuthorizationServiceView {
+ private static final String CLIENTSECRET_KEY = "clientsecret";
+ private static final String CLIENTID_KEY = "clientid";
+ private static final String TOKENENDPOINT_KEY = "tokenendpoint";
+ private static final String AUTHENTICATION_SERVICE_KEY = "authentication-service";
+
+ private final ServiceBinding binding;
+
+ public AemAuthorizationServiceView(ServiceBinding binding) {
+ this.binding = binding;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getAuthorizationService() {
+ if (binding.getCredentials().containsKey(AUTHENTICATION_SERVICE_KEY)) {
+ return (Map) binding.getCredentials().get(AUTHENTICATION_SERVICE_KEY);
+ } else {
+ return Map.of();
+ }
+ }
+
+ /**
+ * Checks if the token endpoint, client ID, and client secret are present.
+ *
+ * @return {@code true} if the token endpoint, client ID, and client secret are
+ * present; {@code false} otherwise.
+ */
+ public boolean isTokenEndpointPresent() {
+ return getTokenEndpoint().isPresent() && getClientId().isPresent() && getClientSecret().isPresent();
+ }
+
+ /**
+ * Retrieves the token endpoint URL from the authorization service.
+ *
+ * @return an {@link Optional} containing the token endpoint URL if present,
+ * otherwise an empty {@link Optional}.
+ */
+ public Optional getTokenEndpoint() {
+ return Optional.ofNullable((String) this.getAuthorizationService().get(TOKENENDPOINT_KEY));
+ }
+
+ /**
+ * Retrieves the client ID from the authorization service.
+ *
+ * @return an {@link Optional} containing the client ID if present, otherwise an
+ * empty {@link Optional}
+ */
+ public Optional getClientId() {
+ return Optional.ofNullable((String) this.getAuthorizationService().get(CLIENTID_KEY));
+ }
+
+ /**
+ * Retrieves the client secret from the authorization service.
+ *
+ * @return an {@link Optional} containing the client secret if present,
+ * otherwise an empty {@link Optional}
+ */
+ public Optional getClientSecret() {
+ return Optional.ofNullable((String) this.getAuthorizationService().get(CLIENTSECRET_KEY));
+ }
+}
\ No newline at end of file
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java
new file mode 100644
index 0000000..3a5d4d8
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java
@@ -0,0 +1,78 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Optional;
+
+import com.sap.cds.feature.messaging.aem.service.AemMessagingServiceConfiguration;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+
+/**
+ * AemEndpointView is a class that provides access to the AMQP URI and URI of an
+ * AEM endpoint
+ * from a given ServiceBinding. It retrieves the endpoint information from the
+ * service binding's
+ * credentials.
+ */
+public class AemEndpointView {
+ private static final String ENDPOINTS_KEY = "endpoints";
+ private static final String AMQP_URI_KEY = "amqp_uri";
+ private static final String URI_KEY = "uri";
+
+ private final ServiceBinding binding;
+
+ public AemEndpointView(ServiceBinding binding) {
+ this.binding = binding;
+ }
+
+ /**
+ * Retrieves the URI from the AEM endpoint.
+ *
+ * @return an {@link Optional} containing the URI if present, otherwise an empty
+ * {@link Optional}
+ */
+ public Optional getUri() {
+ return Optional.ofNullable((String) getAemEndpoint().get(URI_KEY));
+ }
+
+ /**
+ * Retrieves the AMQP URI from the AEM endpoint.
+ *
+ * @return an {@link Optional} containing the AMQP URI if present, otherwise an
+ * empty {@link Optional}.
+ */
+ public Optional getAmqpUriKey() {
+ return Optional.ofNullable((String) getAemEndpoint().get(AMQP_URI_KEY));
+ }
+
+ /**
+ * Retrieves the VPN value from the binding credentials.
+ *
+ * @return an {@link Optional} containing the VPN value if present, otherwise an
+ * empty {@link Optional}
+ */
+ public Optional getVpn() {
+ return Optional.ofNullable((String) this.binding.getCredentials().get("vpn"));
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getEndpointsKey() {
+ Map endpoints = (Map) binding.getCredentials().get(ENDPOINTS_KEY);
+
+ return endpoints != null ? endpoints : Map.of();
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getAemEndpoint() {
+ Map endpoints = getEndpointsKey();
+ Iterator> iterator = endpoints.values().iterator();
+ Map aemEndpoint = iterator.hasNext() ? (Map) iterator.next() : null;
+
+ if (endpoints.containsKey(AemMessagingServiceConfiguration.BINDING_AEM_LABEL) && aemEndpoint != null) {
+ return aemEndpoint;
+ } else {
+ return Map.of();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemOauth2PropertySupplier.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemOauth2PropertySupplier.java
new file mode 100644
index 0000000..efcfa3e
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemOauth2PropertySupplier.java
@@ -0,0 +1,101 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Optional;
+
+import javax.annotation.Nonnull;
+
+import com.sap.cds.feature.messaging.aem.service.AemMessagingServiceConfiguration;
+import com.sap.cds.services.ServiceException;
+import com.sap.cds.services.utils.environment.ServiceBindingUtils;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2PropertySupplier;
+import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2ServiceBindingDestinationLoader;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+
+public class AemOauth2PropertySupplier implements OAuth2PropertySupplier {
+
+ private static boolean initialized = false;
+
+ private final ServiceBinding binding;
+ private final AemAuthorizationServiceView authorizationServiceView;
+ private final AemEndpointView endpointView;
+
+ public static synchronized void initialize() {
+ if (!initialized) {
+ OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(
+ options -> ServiceBindingUtils.matches(options.getServiceBinding(),
+ AemMessagingServiceConfiguration.BINDING_AEM_LABEL),
+ AemOauth2PropertySupplier::new);
+ initialized = true;
+ }
+ }
+
+ public AemOauth2PropertySupplier(@Nonnull ServiceBindingDestinationOptions options) {
+ this.binding = options.getServiceBinding();
+ this.authorizationServiceView = new AemAuthorizationServiceView(binding);
+ this.endpointView = new AemEndpointView(binding);
+ }
+
+ @Override
+ public boolean isOAuth2Binding() {
+ return isAemBinding(this.binding) && areOAuth2ParametersPresent(this.binding);
+ }
+
+ @Nonnull
+ @Override
+ public URI getServiceUri() {
+ String uri = endpointView.getUri()
+ .orElseThrow(() -> new ServiceException("Service endpoint not found in binding"));
+
+ try {
+ return new URI(uri);
+ } catch (URISyntaxException e) {
+ throw new ServiceException("Invalid Service URI: " + uri, e);
+ }
+ }
+
+ @Nonnull
+ @Override
+ public URI getTokenUri() {
+ try {
+ return new URI(this.authorizationServiceView.getTokenEndpoint().get());
+ } catch (URISyntaxException e) {
+ throw new ServiceException(e.getMessage(), e);
+ }
+ }
+
+ @Nonnull
+ @Override
+ public com.sap.cloud.security.config.ClientIdentity getClientIdentity() {
+ return new ClientIdentity(this.authorizationServiceView.getClientId().get(),
+ this.authorizationServiceView.getClientSecret().get());
+ }
+
+ private boolean isAemBinding(ServiceBinding binding) {
+ Optional serviceName = binding.getName();
+
+ return serviceName.map(name -> name.equals(AemMessagingServiceConfiguration.BINDING_AEM_LABEL)).orElse(false);
+ }
+
+ private boolean areOAuth2ParametersPresent(ServiceBinding binding) {
+ return this.authorizationServiceView.getTokenEndpoint().isPresent()
+ && this.authorizationServiceView.getClientId().isPresent()
+ && this.authorizationServiceView.getClientSecret().isPresent();
+ }
+
+ private record ClientIdentity(String clientId, String clientSecret)
+ implements com.sap.cloud.security.config.ClientIdentity {
+
+ @Override
+ public String getId() {
+ return this.clientId;
+ }
+
+ @Override
+ public String getSecret() {
+ return this.clientSecret;
+ }
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
new file mode 100644
index 0000000..46d1c27
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
@@ -0,0 +1,45 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.http.HttpRequest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.sap.cds.integration.cloudsdk.rest.client.JsonRestClient;
+import com.sap.cloud.security.xsuaa.http.HttpHeaders;
+import com.sap.cloud.security.xsuaa.http.MediaType;
+
+public class AemTokenFetchClient extends JsonRestClient {
+
+ public AemTokenFetchClient(String destinationName) {
+ super(destinationName, "");
+ }
+
+ public Optional fetchToken() throws IOException {
+ //JsonNode response = this.getRequest("?grant_type=client_credentials");
+ String requestBody = String.format(
+ "grant_type=client_credentials&client_id=%s&client_secret=%s",
+ URLEncoder.encode("a5fe563c-76a3-4507-b107-63f2a112ee4a", StandardCharsets.UTF_8),
+ URLEncoder.encode("ch80Nj]-L[vw[EjZyPB1D-:iRW._:Mm0=", StandardCharsets.UTF_8));
+ JsonNode response = this.postRequest(
+ "",
+ requestBody,
+ Map.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED.value(),
+ HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.value()));
+ JsonNode accessTokenNode = response.get("access_token");
+
+ return Optional.ofNullable(accessTokenNode).map(JsonNode::asText);
+ }
+
+ @Override
+ protected void setHeaders(HttpRequest request) {
+ super.setHeaders(request);
+
+ request.removeHeaders(CONTENT_TYPE);
+ request.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED.value());
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
new file mode 100644
index 0000000..7a9b491
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
@@ -0,0 +1,120 @@
+package com.sap.cds.feature.messaging.aem.jms;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Map;
+import java.util.function.BiFunction;
+
+import org.apache.qpid.jms.JmsConnectionExtensions;
+import org.apache.qpid.jms.JmsConnectionFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sap.cds.feature.messaging.aem.client.binding.AemAuthorizationServiceView;
+import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView;
+import com.sap.cds.feature.messaging.aem.client.binding.AemTokenFetchClient;
+import com.sap.cds.services.ServiceException;
+import com.sap.cds.services.messaging.jms.BrokerConnection;
+import com.sap.cds.services.messaging.jms.BrokerConnectionProvider;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+import com.sap.cloud.sdk.cloudplatform.connectivity.AuthenticationType;
+import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultDestinationLoader;
+import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
+import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
+import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
+import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
+import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
+
+import jakarta.jms.Connection;
+
+public class AemMessagingConnectionProvider extends BrokerConnectionProvider {
+
+ private static final Logger logger = LoggerFactory.getLogger(AemMessagingConnectionProvider.class);
+
+ private static final String AEM_TOKEN_FETCH_DESTINATION = "x-cap-aem-token-fetch-destination";
+ private static final String SASL_MECHANISM_URI_PARAMETER = "/?amqp.saslMechanisms=XOAUTH2";
+
+ private final ServiceBinding binding;
+ private final AemAuthorizationServiceView authorizationServiceView;
+ private final AemEndpointView endpointView;
+ private AemTokenFetchClient tokenFetchClient;
+
+ public AemMessagingConnectionProvider(ServiceBinding binding) {
+ super(binding.getName().get());
+ this.binding = binding;
+ this.authorizationServiceView = new AemAuthorizationServiceView(binding);
+ this.endpointView = new AemEndpointView(binding);
+
+ if (this.authorizationServiceView.isTokenEndpointPresent()) {
+ this.registerTokenFetchDestination();
+ this.tokenFetchClient = new AemTokenFetchClient(AEM_TOKEN_FETCH_DESTINATION);
+ }
+ }
+
+ @Override
+ protected BrokerConnection createBrokerConnection(String name, Map clientProperties) throws Exception {
+ // see https://solace.community/discussion/1677/how-oauth-can-be-used-with-apache-qpid-jms-2-0-amqp
+ logger.debug("Retrieving credentials for Basic Auth from service binding '{}'", binding.getName().get());
+
+ String amqpUri = this.endpointView.getAmqpUriKey().orElseThrow(() -> new ServiceException(
+ "AMQP URI key is missing in the service binding. Please check the service binding configuration."));
+ amqpUri = amqpUri + SASL_MECHANISM_URI_PARAMETER;
+
+ // TODO: What if IAS is used?
+ final BiFunction tokenExtension = new BiFunction<>() {
+ private volatile String token = null;
+ private long currentTokenExpirationTime; // 10 minutes
+
+ @Override
+ public Object apply(final Connection connection, final URI uri) {
+ long currentTime = System.currentTimeMillis();
+
+ if (currentTime > currentTokenExpirationTime) {
+ try {
+ this.token = tokenFetchClient.fetchToken().orElseThrow(() -> new ServiceException("Token is missing"));
+ this.currentTokenExpirationTime = currentTime + 10 * 60 * 1000; // 10 minutes
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ return this.token;
+ }
+ };
+
+ logger.debug("Creating connection factory fo service binding '{}'",
+ this.binding.getName().get());
+ JmsConnectionFactory factory = new JmsConnectionFactory(this.endpointView.getVpn().get(), "token", amqpUri);
+ factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), tokenExtension);
+
+ return new BrokerConnection(name, factory);
+ }
+
+ private void registerTokenFetchDestination() {
+ if (!destinationExists()) {
+ HttpDestination destination = DefaultHttpDestination
+ .builder(this.authorizationServiceView.getTokenEndpoint().get())
+ .name(AEM_TOKEN_FETCH_DESTINATION)
+ // TODO remove the commented code if it working without
+ //.authenticationType(AuthenticationType.NO_AUTHENTICATION)
+ .authenticationType(AuthenticationType.BASIC_AUTHENTICATION)
+ .basicCredentials(this.authorizationServiceView.getClientId().get(), this.authorizationServiceView.getClientSecret().get())
+ .build();
+ DefaultDestinationLoader loader = new DefaultDestinationLoader().registerDestination(destination);
+
+ DestinationAccessor.appendDestinationLoader(loader);
+ }
+ }
+
+ private boolean destinationExists() {
+ try {
+ DestinationAccessor.getDestination(AEM_TOKEN_FETCH_DESTINATION);
+ return true;
+ } catch (DestinationAccessException | DestinationNotFoundException e) {
+ // Empty by design
+ }
+
+ return false;
+ }
+
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java
new file mode 100644
index 0000000..b7a1377
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java
@@ -0,0 +1,114 @@
+package com.sap.cds.feature.messaging.aem.service;
+
+import static com.sap.cds.feature.messaging.aem.client.AemManagementClient.ATTR_DEAD_MSG_QUEUE;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.qpid.jms.message.JmsBytesMessage;
+import org.apache.qpid.jms.message.JmsTextMessage;
+import org.apache.qpid.jms.provider.amqp.message.AmqpJmsBytesMessageFacade;
+import org.apache.qpid.jms.provider.amqp.message.AmqpJmsTextMessageFacade;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sap.cds.feature.messaging.aem.client.AemManagementClient;
+import com.sap.cds.feature.messaging.aem.jms.AemMessagingConnectionProvider;
+import com.sap.cds.services.environment.CdsProperties.Messaging.MessagingServiceConfig;
+import com.sap.cds.services.messaging.TopicMessageEventContext;
+import com.sap.cds.services.messaging.jms.BrokerConnection;
+import com.sap.cds.services.messaging.service.AbstractMessagingService;
+import com.sap.cds.services.messaging.service.MessagingBrokerQueueListener;
+import com.sap.cds.services.runtime.CdsRuntime;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+
+import jakarta.jms.JMSException;
+import jakarta.jms.Message;
+
+public class AemMessagingService extends AbstractMessagingService {
+ private static final Logger logger = LoggerFactory.getLogger(AemMessagingService.class);
+
+ private final AemMessagingConnectionProvider connectionProvider;
+ private final AemManagementClient managementClient;
+
+ private volatile BrokerConnection connection;
+
+ protected AemMessagingService(ServiceBinding binding, MessagingServiceConfig serviceConfig, AemMessagingConnectionProvider connectionProvider, CdsRuntime runtime) {
+ super(serviceConfig, runtime);
+
+ this.connectionProvider = connectionProvider;
+ this.managementClient = new AemManagementClient(binding);
+ }
+
+ @Override
+ public void init() {
+ logger.debug("Creating the broker connection asynchronously with topic subscriptions.");
+ connectionProvider.asyncConnectionInitialization(serviceConfig, connection -> {
+ this.connection = connection;
+ super.init();
+
+ logger.debug("The broker connection has been created.");
+ });
+ }
+
+ @Override
+ public void stop() {
+ logger.debug("Stopping the broker connection...");
+
+ if (connection != null) {
+ try {
+ connection.close();
+ logger.debug("The broker connection has been stopped.");
+ } catch (JMSException e) {
+ // ignored
+ logger.debug("An error occurred while stopping the broker connection.", e);
+ }
+ } else {
+ logger.debug("No broker connection available..");
+ }
+ }
+
+ @Override
+ protected void removeQueue(String name) throws IOException {
+ managementClient.removeQueue(name);
+ }
+
+ @Override
+ protected void createQueue(String name, Map properties) throws IOException {
+ if (properties.containsKey(ATTR_DEAD_MSG_QUEUE)) {
+ String dmQueue = (String) properties.get(ATTR_DEAD_MSG_QUEUE);
+ if (managementClient.getQueue(dmQueue) == null) {
+ managementClient.createQueue(dmQueue, Collections.emptyMap());
+ }
+ }
+ managementClient.createQueue(name, properties);
+ }
+
+ @Override
+ protected void createQueueSubscription(String queue, String topic) throws IOException {
+ managementClient.createQueueSubscription(queue, topic);
+ }
+
+ @Override
+ protected void registerQueueListener(String queue, MessagingBrokerQueueListener listener) throws IOException {
+ connection.registerQueueListener(queue, listener, this::getMessageTopic);
+ }
+
+ @Override
+ protected void emitTopicMessage(String topic, TopicMessageEventContext messageEventContext) {
+ connection.emitTopicMessage("topic://" + topic, messageEventContext);
+ }
+
+ private String getMessageTopic(Message message) {
+ if (message instanceof JmsTextMessage textMessage) {
+ if (textMessage.getFacade() instanceof AmqpJmsTextMessageFacade textMessageFacade) {
+ return textMessageFacade.getDestination().getAddress();
+ }
+ } else if (message instanceof JmsBytesMessage bytesMessage
+ && bytesMessage.getFacade() instanceof AmqpJmsBytesMessageFacade bytesMessageFacade) {
+ return bytesMessageFacade.getDestination().getAddress();
+ }
+ return null;
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfiguration.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfiguration.java
new file mode 100644
index 0000000..ff8411f
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfiguration.java
@@ -0,0 +1,126 @@
+package com.sap.cds.feature.messaging.aem.service;
+
+import static com.sap.cds.services.messaging.utils.MessagingOutboxUtils.outboxed;
+
+import java.util.List;
+import java.util.Objects;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sap.cds.feature.messaging.aem.client.binding.AemOauth2PropertySupplier;
+import com.sap.cds.feature.messaging.aem.jms.AemMessagingConnectionProvider;
+import com.sap.cds.services.environment.CdsProperties.Messaging;
+import com.sap.cds.services.environment.CdsProperties.Messaging.MessagingServiceConfig;
+import com.sap.cds.services.messaging.MessagingService;
+import com.sap.cds.services.runtime.CdsRuntime;
+import com.sap.cds.services.runtime.CdsRuntimeConfiguration;
+import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
+import com.sap.cds.services.utils.StringUtils;
+import com.sap.cds.services.utils.environment.ServiceBindingUtils;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+
+public class AemMessagingServiceConfiguration implements CdsRuntimeConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(AemMessagingServiceConfiguration.class);
+
+ public static final String BINDING_AEM_LABEL = "advanced-event-mesh";
+ public static final String AEM_KIND = "advanced-event-mesh";
+
+ @Override
+ public void services(CdsRuntimeConfigurer configurer) {
+ AemOauth2PropertySupplier.initialize();
+
+ Messaging config = configurer.getCdsRuntime().getEnvironment().getCdsProperties().getMessaging();
+ List bindings = configurer.getCdsRuntime().getEnvironment().getServiceBindings()
+ .filter(binding -> ServiceBindingUtils.matches(binding, BINDING_AEM_LABEL))
+ .toList();
+
+ if (bindings.isEmpty()) {
+ logger.info("No service bindings with name '{}' found", BINDING_AEM_LABEL);
+ } else {
+ boolean isSingleBinding = bindings.size() == 1;
+
+ bindings.forEach(binding -> {
+
+ logger.debug("Starting the initialization of the advanced-event-mesh service binding '{}'",
+ binding.getName().get());
+
+ AemMessagingConnectionProvider sharedConnectionProvider = new AemMessagingConnectionProvider(binding);
+
+ // determines whether no configuration is available and the default service
+ // should be created
+ boolean createDefaultService = true;
+
+ // check the services configured by binding
+ List serviceConfigs = config.getServicesByBinding(binding.getName().get());
+
+ if (!serviceConfigs.isEmpty()) {
+ logger.debug("Service configurations found, not creating default service.");
+
+ createDefaultService = false;
+ serviceConfigs.forEach(serviceConfig -> {
+ if (serviceConfig.isEnabled()) {
+ // register the service
+ configurer.service(createMessagingService(binding, sharedConnectionProvider, serviceConfig,
+ configurer.getCdsRuntime(), Objects.equals(AEM_KIND, serviceConfig.getKind())));
+ }
+ });
+ }
+
+ // check the services configured by kind if only one service binding is
+ // available
+ logger.debug("Checking the services configured by kind if only one service binding is available.");
+ List serviceConfigsByKind = config.getServicesByKind(BINDING_AEM_LABEL);
+ serviceConfigsByKind.addAll(config.getServicesByKind(AEM_KIND));
+
+ if (isSingleBinding && !serviceConfigsByKind.isEmpty()) {
+ logger.debug(
+ "Service configurations by kind for single service binding found, not creating default service.");
+ createDefaultService = false;
+ serviceConfigsByKind.forEach(serviceConfig -> {
+ // check that the service is enabled and whether not already found by name or
+ // binding
+ if (serviceConfig.isEnabled() && serviceConfigs.stream()
+ .noneMatch(c -> c.getName().equals(serviceConfig.getName()))) {
+ // register the service
+ configurer.service(createMessagingService(binding, sharedConnectionProvider, serviceConfig,
+ configurer.getCdsRuntime(), Objects.equals(AEM_KIND, serviceConfig.getKind())));
+ }
+ });
+ }
+
+ if (createDefaultService) {
+ logger.debug("No service configurations found, creating default service.");
+
+ // otherwise create default service instance for the binding
+ MessagingServiceConfig defConfig = config.getService(binding.getName().get());
+ if (StringUtils.isEmpty(defConfig.getBinding()) && StringUtils.isEmpty(defConfig.getKind())) {
+ // register the service
+ configurer.service(createMessagingService(binding, sharedConnectionProvider, defConfig,
+ configurer.getCdsRuntime(), false));
+ } else {
+ logger.warn(
+ "Could not create service for binding '{}': A configuration with the same name is already defined for another kind or binding.",
+ binding.getName().get());
+ }
+ }
+
+ logger.debug("Finished the initialization of the advanced-event-mesh service binding '{}'",
+ binding.getName().get());
+ });
+ }
+ }
+
+ private MessagingService createMessagingService(ServiceBinding binding,
+ AemMessagingConnectionProvider sharedConnectionProvider, MessagingServiceConfig serviceConfig,
+ CdsRuntime runtime, boolean forceShared) {
+ MessagingService service = new AemMessagingService(binding, serviceConfig, sharedConnectionProvider, runtime);
+
+ logger.debug("Created messaging service '{}' for binding '{}'", serviceConfig.getName(),
+ binding.getName().get());
+
+ return outboxed(service, serviceConfig, runtime);
+ }
+
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration b/cds-feature-advanced-event-mesh/src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration
new file mode 100644
index 0000000..e7447c7
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/resources/META-INF/services/com.sap.cds.services.runtime.CdsRuntimeConfiguration
@@ -0,0 +1 @@
+com.sap.cds.feature.messaging.aem.service.AemMessagingServiceConfiguration
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/AemMessagingServiceConfigurationTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/AemMessagingServiceConfigurationTest.java
new file mode 100644
index 0000000..4f0bbc2
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/AemMessagingServiceConfigurationTest.java
@@ -0,0 +1,113 @@
+package com.sap.cds.feature.messaging.aem;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import com.sap.cds.feature.messaging.aem.service.AemMessagingService;
+import com.sap.cds.services.environment.CdsProperties;
+import com.sap.cds.services.environment.CdsProperties.Messaging.MessagingServiceConfig;
+import com.sap.cds.services.impl.environment.SimplePropertiesProvider;
+import com.sap.cds.services.runtime.CdsRuntimeConfigurer;
+
+public class AemMessagingServiceConfigurationTest {
+
+ @Test
+ void testDefaultServConfiguration() {
+ CdsProperties properties = new CdsProperties();
+ properties.getOutbox().getInMemory().setEnabled(false);
+ CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(properties));
+
+ configurer.serviceConfigurations();
+ configurer.eventHandlerConfigurations();
+
+ List services = configurer.getCdsRuntime().getServiceCatalog().getServices(AemMessagingService.class).toList();
+
+ assertEquals(1, services.size());
+ assertEquals("advanced-event-mesh", services.get(0).getName());
+ }
+
+ @Test
+ void testNoServiceBindings() {
+ CdsProperties properties = new CdsProperties();
+ CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(properties));
+
+ configurer.serviceConfigurations();
+ configurer.eventHandlerConfigurations();
+
+ List services = configurer.getCdsRuntime().getServiceCatalog().getServices(AemMessagingService.class).toList();
+ assertTrue(services.isEmpty());
+ }
+
+ @Test
+ void testSingleServConfiguration() {
+ CdsProperties properties = new CdsProperties();
+ MessagingServiceConfig config = new MessagingServiceConfig("cfg");
+ config.setBinding("advanced-event-mesh");
+ config.getOutbox().setEnabled(false);
+ properties.getMessaging().getServices().put(config.getName(), config);
+
+ assertEquals(0, properties.getMessaging().getServicesByBinding("").size());
+ assertEquals(1, properties.getMessaging().getServicesByBinding("advanced-event-mesh").size());
+ assertEquals(0, properties.getMessaging().getServicesByKind("").size());
+ assertEquals(0, properties.getMessaging().getServicesByKind("aem").size());
+
+ CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(properties));
+
+ configurer.serviceConfigurations();
+ configurer.eventHandlerConfigurations();
+
+ List services = configurer.getCdsRuntime().getServiceCatalog().getServices(AemMessagingService.class).collect(Collectors.toList());
+
+ assertEquals(1, services.size());
+ assertEquals("cfg", services.get(0).getName());
+ }
+
+ @Test
+ void testMultipleServiceConfigurations() {
+ CdsProperties properties = new CdsProperties();
+ MessagingServiceConfig config1 = new MessagingServiceConfig("cfg1");
+ config1.setBinding("advanced-event-mesh");
+ config1.getOutbox().setEnabled(false);
+
+ MessagingServiceConfig config2 = new MessagingServiceConfig("cfg2");
+ config2.setBinding("advanced-event-mesh");
+ config2.getOutbox().setEnabled(false);
+
+ properties.getMessaging().getServices().put(config1.getName(), config1);
+ properties.getMessaging().getServices().put(config2.getName(), config2);
+
+ CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(properties));
+
+ configurer.serviceConfigurations();
+ configurer.eventHandlerConfigurations();
+
+ List services = configurer.getCdsRuntime().getServiceCatalog().getServices(AemMessagingService.class).collect(Collectors.toList());
+
+ assertEquals(2, services.size());
+ assertTrue(services.stream().anyMatch(service -> "cfg1".equals(service.getName())));
+ assertTrue(services.stream().anyMatch(service -> "cfg2".equals(service.getName())));
+ }
+
+ @Test
+ void testServiceConfigurationWithInvalidBinding() {
+ CdsProperties properties = new CdsProperties();
+ MessagingServiceConfig config = new MessagingServiceConfig("cfg");
+ config.setBinding("invalid-binding");
+ config.getOutbox().setEnabled(false);
+ properties.getMessaging().getServices().put(config.getName(), config);
+
+ CdsRuntimeConfigurer configurer = CdsRuntimeConfigurer.create(new SimplePropertiesProvider(properties));
+
+ configurer.serviceConfigurations();
+ configurer.eventHandlerConfigurations();
+
+ List services = configurer.getCdsRuntime().getServiceCatalog().getServices(AemMessagingService.class).collect(Collectors.toList());
+
+ assertTrue(services.isEmpty(), "Expected no services to be configured with invalid binding");
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/test/resources/default-env.json b/cds-feature-advanced-event-mesh/src/test/resources/default-env.json
new file mode 100644
index 0000000..0bda1a3
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/test/resources/default-env.json
@@ -0,0 +1,33 @@
+{
+ "VCAP_SERVICES": {
+ "advanced-event-mesh": [
+ {
+ "label": "advanced-event-mesh",
+ "name": "advanced-event-mesh",
+ "tags": [],
+ "instance_guid": "515066ab-029d-4cdd-a116-a2c254633b6b",
+ "instance_name": "advanced-event-mesh",
+ "binding_guid": "5aec22e5-8380-47a2-99ae-1c4011cdcf88",
+ "binding_name": null,
+ "credentials": {
+ "vpn": "capservice",
+ "authentication-service": {
+ "tokenendpoint": "https://ad8obf0da.accounts400.ondemand.com/oauth2/token",
+ "clientid": "a5fe563c-76a3-4507-b107-63f2a112ee4a",
+ "clientsecret": "ch80Nj]-L[vw[EjZyPB1D-:iRW._:Mm0="
+ },
+ "endpoints": {
+ "advanced-event-mesh": {
+ "uri": "https://mr-connection-i90693rt5n6.messaging.solace.cloud:943/SEMP/v2/config",
+ "always-requires-token": true,
+ "amqp_uri": "amqps://mr-connection-i90693rt5n6.messaging.solace.cloud:5671",
+ "smf_uri": "wss://mr-connection-i90693rt5n6.messaging.solace.cloud:443"
+ }
+ }
+ },
+ "syslog_drain_url": null,
+ "volume_mounts": []
+ }
+ ]
+ }
+}
diff --git a/deploy-oss/pom.xml b/deploy-oss/pom.xml
new file mode 100644
index 0000000..bd91590
--- /dev/null
+++ b/deploy-oss/pom.xml
@@ -0,0 +1,96 @@
+
+ 4.0.0
+ com.sap.cds
+ deploy-oss
+ ${revision}
+ pom
+
+ Deploy to OSS
+ This artifact can be used to deploy all required artifacts of cds-feature-advanced-event-mesh to OSS Nexus
+
+
+ true
+ true
+
+
+
+
+
+ maven-install-plugin
+ 3.1.4
+
+
+ install-cds-feature-advanced-event-mesh
+ install
+
+ install-file
+
+
+ ${project.groupId}
+ cds-feature-advanced-event-mesh
+ jar
+ ../cds-feature-advanced-event-mesh/target/cds-feature-advanced-event-mesh.jar
+ ../cds-feature-advanced-event-mesh/target/cds-feature-advanced-event-mesh-sources.jar
+ ../cds-feature-advanced-event-mesh/target/cds-feature-advanced-event-mesh-javadoc.jar
+ ../cds-feature-advanced-event-mesh/.flattened-pom.xml
+ ${revision}
+
+
+
+ install-cds-feature-advanced-event-mesh-root
+ install
+
+ install-file
+
+
+ ${project.groupId}
+ cds-feature-advanced-event-mesh-root
+ pom
+ ../.flattened-pom.xml
+ ../.flattened-pom.xml
+ ${revision}
+
+
+
+
+
+ maven-gpg-plugin
+ 3.2.7
+
+
+ deploy-cds-feature-advanced-event-mesh
+ deploy
+
+ sign-and-deploy-file
+
+
+ ${project.groupId}
+ cds-feature-advanced-event-mesh
+ jar
+ ../cds-feature-advanced-event-mesh/target/cds-feature-advanced-event-mesh.jar
+ ../cds-feature-advanced-event-mesh/target/cds-feature-advanced-event-mesh-sources.jar
+ ../cds-feature-advanced-event-mesh/target/cds-feature-advanced-event-mesh-javadoc.jar
+ ../cds-feature-advanced-event-mesh/.flattened-pom.xml
+ ${revision}
+
+
+
+ deploy-cds-feature-advanced-event-mesh-root
+ deploy
+
+ sign-and-deploy-file
+
+
+ ${project.groupId}
+ cds-feature-advanced-event-mesh-root
+ pom
+ ../.flattened-pom.xml
+ ../.flattened-pom.xml
+ ${revision}
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..cbcff75
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,258 @@
+
+ 4.0.0
+
+
+
+ The Apache Software License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ SAP SE
+ https://www.sap.com
+
+
+
+
+ SAP SE
+ https://www.sap.com
+
+
+
+
+ 4.1.0-SNAPSHOT
+ 17
+ ${java.version}
+ UTF-8
+
+ 3.9.0-SNAPSHOT
+ 8.7.3
+ 2.7.0
+
+
+ com.sap.cds
+ cds-feature-advanced-event-mesh-root
+ ${revision}
+ pom
+
+ CDS plugin for SAP Integration Suite, advanced event mesh - Root
+ This artifact is a CAP Java plugin that provides out-of-the box integration with SAP Integration Suite, advanced event mesh.
+ https://cap.cloud.sap/docs/plugins/#event-broker-plugin
+
+
+ cds-feature-advanced-event-mesh
+
+
+
+
+
+ com.sap.cds
+ cds-services-bom
+ ${cds.services.version}
+ pom
+ import
+
+
+
+ com.sap.cloud.sdk
+ sdk-bom
+ 5.17.0
+ pom
+ import
+
+
+
+ org.mockito
+ mockito-bom
+ 5.16.0
+ pom
+ import
+
+
+
+ com.sap.cds
+ cds-feature-advanced-event-mesh
+ ${revision}
+
+
+
+ org.apache.qpid
+ qpid-jms-client
+ ${qpid.version}
+
+
+
+
+ commons-codec
+ commons-codec
+ 1.18.0
+
+
+
+
+
+
+ com.sap.cds
+ cds-services-api
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.12.0
+ test
+
+
+
+ org.assertj
+ assertj-core
+ 3.27.3
+ test
+
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+
+
+
+
+
+ maven-surefire-plugin
+
+
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+
+ true
+ resolveCiFriendliesOnly
+
+
+
+ flatten
+ process-resources
+
+ flatten
+
+
+
+ flatten.clean
+ clean
+
+ clean
+
+
+
+
+
+
+ maven-enforcer-plugin
+
+
+ no-duplicate-declared-dependencies
+
+ enforce
+
+
+
+
+
+ 3.6.3
+
+
+ ${java.version}
+
+
+
+
+
+
+
+
+
+
+
+
+ maven-clean-plugin
+ 3.4.1
+
+
+ maven-compiler-plugin
+ 3.14.0
+
+
+ maven-source-plugin
+ 3.3.1
+
+
+ maven-deploy-plugin
+ 3.1.4
+
+
+ maven-javadoc-plugin
+ 3.11.2
+
+
+ maven-surefire-plugin
+ 3.5.2
+
+
+ maven-pmd-plugin
+ 3.26.0
+
+
+ maven-enforcer-plugin
+ 3.5.0
+
+
+ org.codehaus.mojo
+ flatten-maven-plugin
+ 1.7.0
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.12
+
+
+ org.pitest
+ pitest-maven
+ 1.18.2
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ 4.9.2.0
+
+
+
+
+
+
+
+ artifactory
+ Artifactory_DMZ-snapshots
+ https://common.repositories.cloud.sap/artifactory/cap-java
+
+
+ ossrh
+ MavenCentral
+ https://oss.sonatype.org/service/local/staging/deploy/maven2/
+
+
+
+
+ https://github.com/cap-java/advanced-event-mesh
+ scm:git:git@github.com:cap-java/advanced-event-mesh.git
+ scm:git:git@github.com:cap-java/advanced-event-mesh.git
+
+
+
From 7292c24a4950d80221a4dbc3101499aa2c8b8b74 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Fri, 14 Mar 2025 09:41:21 +0100
Subject: [PATCH 02/32] changed string
---
.../messaging/aem/client/binding/AemTokenFetchClient.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
index 46d1c27..11837ff 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
@@ -23,8 +23,8 @@ public Optional fetchToken() throws IOException {
//JsonNode response = this.getRequest("?grant_type=client_credentials");
String requestBody = String.format(
"grant_type=client_credentials&client_id=%s&client_secret=%s",
- URLEncoder.encode("a5fe563c-76a3-4507-b107-63f2a112ee4a", StandardCharsets.UTF_8),
- URLEncoder.encode("ch80Nj]-L[vw[EjZyPB1D-:iRW._:Mm0=", StandardCharsets.UTF_8));
+ URLEncoder.encode("TODO", StandardCharsets.UTF_8),
+ URLEncoder.encode("TODO", StandardCharsets.UTF_8));
JsonNode response = this.postRequest(
"",
requestBody,
From 4c2430f7472930912bf303861ac55482013339f5 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Fri, 14 Mar 2025 11:35:22 +0100
Subject: [PATCH 03/32] changed AemTokenFetchClient
---
cds-feature-advanced-event-mesh/pom.xml | 2 +-
.../aem/client/AemManagementClient.java | 7 +-
.../client/binding/AemTokenFetchClient.java | 94 ++++++++++++++-----
.../jms/AemMessagingConnectionProvider.java | 51 +---------
pom.xml | 2 +-
5 files changed, 82 insertions(+), 74 deletions(-)
diff --git a/cds-feature-advanced-event-mesh/pom.xml b/cds-feature-advanced-event-mesh/pom.xml
index 2dc881e..b0298bf 100644
--- a/cds-feature-advanced-event-mesh/pom.xml
+++ b/cds-feature-advanced-event-mesh/pom.xml
@@ -12,7 +12,7 @@
jar
CDS plugin for SAP Integration Suite, advanced event mesh
- https://cap.cloud.sap/docs/plugins/#event-broker-plugin
+ https://cap.cloud.sap/docs/plugins/#advanced-event-mesh-plugin
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
index b1172ab..9075afa 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
@@ -35,14 +35,13 @@ public class AemManagementClient extends JsonRestClient {
private final ServiceBinding binding;
private final String vpn;
- private final String owner; // TODO Remove?
+ private final String owner;
public AemManagementClient(ServiceBinding binding) {
super(ServiceBindingDestinationOptions.forService(binding).build());
this.binding = binding;
this.vpn = getVpn();
- //this.owner = getOwner();
- this.owner = this.vpn;
+ this.owner = getOwner();
}
public void removeQueue(String queue) throws IOException {
@@ -144,7 +143,7 @@ private String getVpn() {
}
private String getOwner() {
- return (String) this.binding.getCredentials().get("user");
+ return getVpn() ;
}
}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
index 11837ff..b7bb6df 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
@@ -3,43 +3,93 @@
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
-import java.util.Map;
import java.util.Optional;
-import org.apache.http.HttpRequest;
+import org.apache.http.client.ClientProtocolException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.JsonNode;
-import com.sap.cds.integration.cloudsdk.rest.client.JsonRestClient;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sap.cds.services.ServiceException;
import com.sap.cloud.security.xsuaa.http.HttpHeaders;
import com.sap.cloud.security.xsuaa.http.MediaType;
-public class AemTokenFetchClient extends JsonRestClient {
+public class AemTokenFetchClient {
- public AemTokenFetchClient(String destinationName) {
- super(destinationName, "");
+ private static final Logger logger = LoggerFactory.getLogger(AemTokenFetchClient.class);
+
+ private final AemAuthorizationServiceView authorizationServiceView;
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public AemTokenFetchClient(AemAuthorizationServiceView authorizationServiceView) {
+ this.authorizationServiceView = authorizationServiceView;
}
public Optional fetchToken() throws IOException {
- //JsonNode response = this.getRequest("?grant_type=client_credentials");
String requestBody = String.format(
"grant_type=client_credentials&client_id=%s&client_secret=%s",
- URLEncoder.encode("TODO", StandardCharsets.UTF_8),
- URLEncoder.encode("TODO", StandardCharsets.UTF_8));
- JsonNode response = this.postRequest(
- "",
- requestBody,
- Map.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED.value(),
- HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.value()));
- JsonNode accessTokenNode = response.get("access_token");
-
- return Optional.ofNullable(accessTokenNode).map(JsonNode::asText);
+ URLEncoder.encode(this.authorizationServiceView.getClientId().get(), StandardCharsets.UTF_8),
+ URLEncoder.encode(this.authorizationServiceView.getClientSecret().get(), StandardCharsets.UTF_8));
+
+ HttpPost post = new HttpPost(this.authorizationServiceView.getTokenEndpoint().get());
+ post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED.value());
+ post.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.value());
+
+ post.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
+
+ try(CloseableHttpResponse response = handleRequest(post)) {
+ JsonNode jsonNode = handleJsonResponse(response);
+ JsonNode accessTokenNode = jsonNode.get("access_token");
+
+ return Optional.ofNullable(accessTokenNode).map(JsonNode::asText);
+ } finally {
+ post.releaseConnection();
+ }
}
- @Override
- protected void setHeaders(HttpRequest request) {
- super.setHeaders(request);
+ private CloseableHttpResponse handleRequest(HttpRequestBase request) throws IOException {
+ try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+ return httpClient.execute(request);
+ } catch (ClientProtocolException e) {
+ throw new ServiceException(e.getMessage(), e);
+ }
+ }
+
+ private JsonNode handleJsonResponse(CloseableHttpResponse response) throws IOException {
+ try (CloseableHttpResponse resp = response){
+ int code = resp.getStatusLine().getStatusCode();
+
+ logger.debug("Responded with status code '{}'", code);
- request.removeHeaders(CONTENT_TYPE);
- request.addHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED.value());
+ if (code >= 200 && code <= 207) {
+ String contentType = MediaType.APPLICATION_JSON.value();
+
+ if (resp.getEntity() != null) {
+ if (resp.getEntity().getContentType() != null) {
+ contentType = resp.getEntity().getContentType().getValue();
+ }
+ if (contentType.startsWith(MediaType.APPLICATION_JSON.value())) {
+ String jsonData = EntityUtils.toString(resp.getEntity());
+ return mapper.readValue(jsonData, JsonNode.class);
+ } else {
+ throw new IOException("Unexpected response format: Expected JSON but found '" + contentType + "'");
+ }
+ } else {
+ return mapper.readValue("{}", JsonNode.class);
+ }
+ } else {
+ String reason = resp.getStatusLine().getReasonPhrase();
+ throw new ServiceException("Unexpected request HTTP response (" + code + ") " + reason);
+ }
+ }
}
+
}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
index 7a9b491..36f8541 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
@@ -17,13 +17,6 @@
import com.sap.cds.services.messaging.jms.BrokerConnection;
import com.sap.cds.services.messaging.jms.BrokerConnectionProvider;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
-import com.sap.cloud.sdk.cloudplatform.connectivity.AuthenticationType;
-import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultDestinationLoader;
-import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination;
-import com.sap.cloud.sdk.cloudplatform.connectivity.DestinationAccessor;
-import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
-import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationAccessException;
-import com.sap.cloud.sdk.cloudplatform.connectivity.exception.DestinationNotFoundException;
import jakarta.jms.Connection;
@@ -35,20 +28,14 @@ public class AemMessagingConnectionProvider extends BrokerConnectionProvider {
private static final String SASL_MECHANISM_URI_PARAMETER = "/?amqp.saslMechanisms=XOAUTH2";
private final ServiceBinding binding;
- private final AemAuthorizationServiceView authorizationServiceView;
private final AemEndpointView endpointView;
- private AemTokenFetchClient tokenFetchClient;
+ private final AemTokenFetchClient tokenFetchClient;
public AemMessagingConnectionProvider(ServiceBinding binding) {
super(binding.getName().get());
this.binding = binding;
- this.authorizationServiceView = new AemAuthorizationServiceView(binding);
this.endpointView = new AemEndpointView(binding);
-
- if (this.authorizationServiceView.isTokenEndpointPresent()) {
- this.registerTokenFetchDestination();
- this.tokenFetchClient = new AemTokenFetchClient(AEM_TOKEN_FETCH_DESTINATION);
- }
+ this.tokenFetchClient = new AemTokenFetchClient(new AemAuthorizationServiceView(binding));
}
@Override
@@ -60,7 +47,6 @@ protected BrokerConnection createBrokerConnection(String name, Map tokenExtension = new BiFunction<>() {
private volatile String token = null;
private long currentTokenExpirationTime; // 10 minutes
@@ -72,7 +58,7 @@ public Object apply(final Connection connection, final URI uri) {
if (currentTime > currentTokenExpirationTime) {
try {
this.token = tokenFetchClient.fetchToken().orElseThrow(() -> new ServiceException("Token is missing"));
- this.currentTokenExpirationTime = currentTime + 10 * 60 * 1000; // 10 minutes
+ this.currentTokenExpirationTime = currentTime + 5 * 60 * 1000; // 5 minutes
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -82,39 +68,12 @@ public Object apply(final Connection connection, final URI uri) {
}
};
- logger.debug("Creating connection factory fo service binding '{}'",
- this.binding.getName().get());
+ logger.debug("Creating connection factory fo service binding '{}'", this.binding.getName().get());
+ // the password is going to be replaced by the token
JmsConnectionFactory factory = new JmsConnectionFactory(this.endpointView.getVpn().get(), "token", amqpUri);
factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), tokenExtension);
return new BrokerConnection(name, factory);
}
- private void registerTokenFetchDestination() {
- if (!destinationExists()) {
- HttpDestination destination = DefaultHttpDestination
- .builder(this.authorizationServiceView.getTokenEndpoint().get())
- .name(AEM_TOKEN_FETCH_DESTINATION)
- // TODO remove the commented code if it working without
- //.authenticationType(AuthenticationType.NO_AUTHENTICATION)
- .authenticationType(AuthenticationType.BASIC_AUTHENTICATION)
- .basicCredentials(this.authorizationServiceView.getClientId().get(), this.authorizationServiceView.getClientSecret().get())
- .build();
- DefaultDestinationLoader loader = new DefaultDestinationLoader().registerDestination(destination);
-
- DestinationAccessor.appendDestinationLoader(loader);
- }
- }
-
- private boolean destinationExists() {
- try {
- DestinationAccessor.getDestination(AEM_TOKEN_FETCH_DESTINATION);
- return true;
- } catch (DestinationAccessException | DestinationNotFoundException e) {
- // Empty by design
- }
-
- return false;
- }
-
}
diff --git a/pom.xml b/pom.xml
index cbcff75..6e4a86b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -40,7 +40,7 @@
CDS plugin for SAP Integration Suite, advanced event mesh - Root
This artifact is a CAP Java plugin that provides out-of-the box integration with SAP Integration Suite, advanced event mesh.
- https://cap.cloud.sap/docs/plugins/#event-broker-plugin
+ https://cap.cloud.sap/docs/plugins/#advanced-event-mesh-plugin
cds-feature-advanced-event-mesh
From 8239fc8082eb3453114f259d57d7fe5fd7011522 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Mon, 17 Mar 2025 16:08:28 +0100
Subject: [PATCH 04/32] Created test cases
---
cds-feature-advanced-event-mesh/pom.xml | 8 +-
.../jms/AemMessagingConnectionProvider.java | 21 ++--
.../aem/service/AemMessagingService.java | 9 +-
.../AemAuthorizationServiceViewTest.java | 109 ++++++++++++++++++
.../client/binding/AemEndpointViewTest.java | 90 +++++++++++++++
.../binding/AemTokenFetchClientTest.java | 72 ++++++++++++
.../AemMessagingConnectionProviderTest.java | 102 ++++++++++++++++
.../AemMessagingServiceConfigurationTest.java | 2 +-
8 files changed, 392 insertions(+), 21 deletions(-)
create mode 100644 cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceViewTest.java
create mode 100644 cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java
create mode 100644 cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java
create mode 100644 cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
rename cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/{ => service}/AemMessagingServiceConfigurationTest.java (98%)
diff --git a/cds-feature-advanced-event-mesh/pom.xml b/cds-feature-advanced-event-mesh/pom.xml
index b0298bf..ef5a126 100644
--- a/cds-feature-advanced-event-mesh/pom.xml
+++ b/cds-feature-advanced-event-mesh/pom.xml
@@ -183,7 +183,7 @@
- ${excluded.generation.package}**/*
+ ${excluded.generation.package}**/*
@@ -222,17 +222,17 @@
INSTRUCTION
COVEREDRATIO
- 0.5
+ 0.3
BRANCH
COVEREDRATIO
- 0.5
+ 0.3
COMPLEXITY
COVEREDRATIO
- 0.5
+ 0.3
CLASS
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
index 36f8541..d1f6953 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
@@ -48,29 +48,22 @@ protected BrokerConnection createBrokerConnection(String name, Map tokenExtension = new BiFunction<>() {
- private volatile String token = null;
- private long currentTokenExpirationTime; // 10 minutes
-
@Override
public Object apply(final Connection connection, final URI uri) {
- long currentTime = System.currentTimeMillis();
-
- if (currentTime > currentTokenExpirationTime) {
- try {
- this.token = tokenFetchClient.fetchToken().orElseThrow(() -> new ServiceException("Token is missing"));
- this.currentTokenExpirationTime = currentTime + 5 * 60 * 1000; // 5 minutes
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
+ try {
+ String token = tokenFetchClient.fetchToken().orElseThrow(() -> new ServiceException("Token is missing"));
- return this.token;
+ return token;
+ } catch (IOException e) {
+ throw new ServiceException(e);
+ }
}
};
logger.debug("Creating connection factory fo service binding '{}'", this.binding.getName().get());
// the password is going to be replaced by the token
JmsConnectionFactory factory = new JmsConnectionFactory(this.endpointView.getVpn().get(), "token", amqpUri);
+
factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), tokenExtension);
return new BrokerConnection(name, factory);
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java
index b7a1377..2481b58 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/service/AemMessagingService.java
@@ -5,6 +5,7 @@
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
+import java.util.function.Consumer;
import org.apache.qpid.jms.message.JmsBytesMessage;
import org.apache.qpid.jms.message.JmsTextMessage;
@@ -44,7 +45,7 @@ protected AemMessagingService(ServiceBinding binding, MessagingServiceConfig ser
@Override
public void init() {
logger.debug("Creating the broker connection asynchronously with topic subscriptions.");
- connectionProvider.asyncConnectionInitialization(serviceConfig, connection -> {
+ this.asyncConnectionInitialization(connection -> {
this.connection = connection;
super.init();
@@ -97,7 +98,11 @@ protected void registerQueueListener(String queue, MessagingBrokerQueueListener
@Override
protected void emitTopicMessage(String topic, TopicMessageEventContext messageEventContext) {
- connection.emitTopicMessage("topic://" + topic, messageEventContext);
+ this.connection.emitTopicMessage("topic://" + topic, messageEventContext);
+ }
+
+ private void asyncConnectionInitialization(Consumer connectionConsumer) {
+ connectionProvider.asyncConnectionInitialization(serviceConfig, connectionConsumer);
}
private String getMessageTopic(Message message) {
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceViewTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceViewTest.java
new file mode 100644
index 0000000..0128c3d
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceViewTest.java
@@ -0,0 +1,109 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+
+public class AemAuthorizationServiceViewTest {
+
+ @Mock
+ private ServiceBinding serviceBinding;
+
+ private AemAuthorizationServiceView authorizationServiceView;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ authorizationServiceView = new AemAuthorizationServiceView(serviceBinding);
+ }
+
+ @Test
+ void testIsTokenEndpointPresent() {
+ Map credentials = Map.of(
+ "authentication-service", Map.of(
+ "tokenendpoint", "http://example.com/token",
+ "clientid", "test-client-id",
+ "clientsecret", "test-client-secret"
+ )
+ );
+ when(serviceBinding.getCredentials()).thenReturn(credentials);
+
+ assertTrue(authorizationServiceView.isTokenEndpointPresent());
+ }
+
+ @Test
+ void testIsTokenEndpointPresent_NotPresent() {
+ when(serviceBinding.getCredentials()).thenReturn(Map.of());
+
+ assertFalse(authorizationServiceView.isTokenEndpointPresent());
+ }
+
+ @Test
+ void testGetTokenEndpoint() {
+ Map credentials = Map.of(
+ "authentication-service", Map.of("tokenendpoint", "http://example.com/token")
+ );
+ when(serviceBinding.getCredentials()).thenReturn(credentials);
+
+ Optional tokenEndpoint = authorizationServiceView.getTokenEndpoint();
+ assertTrue(tokenEndpoint.isPresent());
+ assertEquals("http://example.com/token", tokenEndpoint.get());
+ }
+
+ @Test
+ void testGetTokenEndpoint_NotPresent() {
+ when(serviceBinding.getCredentials()).thenReturn(Map.of());
+
+ Optional tokenEndpoint = authorizationServiceView.getTokenEndpoint();
+ assertFalse(tokenEndpoint.isPresent());
+ }
+
+ @Test
+ void testGetClientId() {
+ Map credentials = Map.of(
+ "authentication-service", Map.of("clientid", "test-client-id")
+ );
+ when(serviceBinding.getCredentials()).thenReturn(credentials);
+
+ Optional clientId = authorizationServiceView.getClientId();
+ assertTrue(clientId.isPresent());
+ assertEquals("test-client-id", clientId.get());
+ }
+
+ @Test
+ void testGetClientId_NotPresent() {
+ when(serviceBinding.getCredentials()).thenReturn(Map.of());
+
+ Optional clientId = authorizationServiceView.getClientId();
+ assertFalse(clientId.isPresent());
+ }
+
+ @Test
+ void testGetClientSecret() {
+ Map credentials = Map.of(
+ "authentication-service", Map.of("clientsecret", "test-client-secret")
+ );
+ when(serviceBinding.getCredentials()).thenReturn(credentials);
+
+ Optional clientSecret = authorizationServiceView.getClientSecret();
+ assertTrue(clientSecret.isPresent());
+ assertEquals("test-client-secret", clientSecret.get());
+ }
+
+ @Test
+ void testGetClientSecret_NotPresent() {
+ when(serviceBinding.getCredentials()).thenReturn(Map.of());
+
+ Optional clientSecret = authorizationServiceView.getClientSecret();
+ assertFalse(clientSecret.isPresent());
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java
new file mode 100644
index 0000000..7b5487f
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java
@@ -0,0 +1,90 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.util.Map;
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+
+public class AemEndpointViewTest {
+
+ @Mock
+ private ServiceBinding serviceBinding;
+
+ private AemEndpointView endpointView;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ endpointView = new AemEndpointView(serviceBinding);
+ }
+
+ @Test
+ void testGetUri() {
+ Map credentials = Map.of(
+ "endpoints", Map.of(
+ "advanced-event-mesh", Map.of("uri", "http://example.com")
+ )
+ );
+ when(serviceBinding.getCredentials()).thenReturn(credentials);
+
+ Optional uri = endpointView.getUri();
+ assertTrue(uri.isPresent());
+ assertEquals("http://example.com", uri.get());
+ }
+
+ @Test
+ void testGetUri_NotPresent() {
+ when(serviceBinding.getCredentials()).thenReturn(Map.of());
+
+ Optional uri = endpointView.getUri();
+ assertFalse(uri.isPresent());
+ }
+
+ @Test
+ void testGetAmqpUriKey() {
+ Map credentials = Map.of(
+ "endpoints", Map.of(
+ "advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")
+ )
+ );
+ when(serviceBinding.getCredentials()).thenReturn(credentials);
+
+ Optional amqpUri = endpointView.getAmqpUriKey();
+ assertTrue(amqpUri.isPresent());
+ assertEquals("amqp://example.com", amqpUri.get());
+ }
+
+ @Test
+ void testGetAmqpUriKey_NotPresent() {
+ when(serviceBinding.getCredentials()).thenReturn(Map.of());
+
+ Optional amqpUri = endpointView.getAmqpUriKey();
+ assertFalse(amqpUri.isPresent());
+ }
+
+ @Test
+ void testGetVpn() {
+ Map credentials = Map.of("vpn", "test-vpn");
+ when(serviceBinding.getCredentials()).thenReturn(credentials);
+
+ Optional vpn = endpointView.getVpn();
+ assertTrue(vpn.isPresent());
+ assertEquals("test-vpn", vpn.get());
+ }
+
+ @Test
+ void testGetVpn_NotPresent() {
+ when(serviceBinding.getCredentials()).thenReturn(Map.of());
+
+ Optional vpn = endpointView.getVpn();
+ assertFalse(vpn.isPresent());
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java
new file mode 100644
index 0000000..f545944
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java
@@ -0,0 +1,72 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.util.Optional;
+
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import com.sap.cds.services.ServiceException;
+
+public class AemTokenFetchClientTest {
+
+ @Mock
+ private AemAuthorizationServiceView authorizationServiceView;
+
+ @Mock
+ private CloseableHttpClient httpClient;
+
+ @Mock
+ private CloseableHttpResponse response;
+
+ @Mock
+ private StatusLine statusLine;
+
+ private AemTokenFetchClient tokenFetchClient;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ tokenFetchClient = new AemTokenFetchClient(authorizationServiceView);
+ }
+
+ @Test
+ void testFetchToken_Failure() throws IOException {
+ when(authorizationServiceView.getClientId()).thenReturn(Optional.of("test-client-id"));
+ when(authorizationServiceView.getClientSecret()).thenReturn(Optional.of("test-client-secret"));
+ when(authorizationServiceView.getTokenEndpoint()).thenReturn(Optional.of("http://example.com/token"));
+
+ when(this.statusLine.getStatusCode()).thenReturn(400);
+ when(response.getStatusLine()).thenReturn(this.statusLine);
+
+ when(response.getStatusLine().getStatusCode()).thenReturn(400);
+ when(response.getStatusLine().getReasonPhrase()).thenReturn("Bad Request");
+ when(httpClient.execute(any())).thenReturn(response);
+
+ assertThrows(ServiceException.class, () -> tokenFetchClient.fetchToken());
+ }
+
+ @Test
+ void testFetchToken_InvalidJsonResponse() throws IOException {
+ when(authorizationServiceView.getClientId()).thenReturn(Optional.of("test-client-id"));
+ when(authorizationServiceView.getClientSecret()).thenReturn(Optional.of("test-client-secret"));
+ when(authorizationServiceView.getTokenEndpoint()).thenReturn(Optional.of("http://example.com/token"));
+
+ String invalidJsonResponse = "Invalid JSON";
+ when(this.statusLine.getStatusCode()).thenReturn(200);
+ when(response.getStatusLine()).thenReturn(this.statusLine);
+
+ when(response.getEntity()).thenReturn(new StringEntity(invalidJsonResponse));
+ when(httpClient.execute(any())).thenReturn(response);
+
+ assertThrows(ServiceException.class, () -> tokenFetchClient.fetchToken());
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
new file mode 100644
index 0000000..0e28dfa
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
@@ -0,0 +1,102 @@
+package com.sap.cds.feature.messaging.aem.jms;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import org.apache.qpid.jms.JmsConnectionFactory;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import com.sap.cds.feature.messaging.aem.client.binding.AemAuthorizationServiceView;
+import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView;
+import com.sap.cds.feature.messaging.aem.client.binding.AemTokenFetchClient;
+import com.sap.cds.services.ServiceException;
+import com.sap.cds.services.messaging.jms.BrokerConnection;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+
+import jakarta.jms.Connection;
+
+public class AemMessagingConnectionProviderTest {
+
+ @Mock
+ private ServiceBinding serviceBinding;
+
+ @Mock
+ private AemEndpointView endpointView;
+
+ @Mock
+ private AemTokenFetchClient tokenFetchClient;
+
+ private AemMessagingConnectionProvider connectionProvider;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ when(serviceBinding.getName()).thenReturn(Optional.of("test-binding"));
+
+ connectionProvider = new AemMessagingConnectionProvider(serviceBinding);
+ }
+
+ @Test
+ void testCreateBrokerConnection_Success() throws Exception {
+ when(endpointView.getAmqpUriKey()).thenReturn(Optional.of("amqp://example.com"));
+ when(endpointView.getVpn()).thenReturn(Optional.of("test-vpn"));
+ when(serviceBinding.getCredentials()).thenReturn(
+ Map.of("endpoints", Map.of("advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")),
+ "vpn", "test-vpn")
+ );
+ when(tokenFetchClient.fetchToken()).thenReturn(Optional.of("test-token"));
+
+ BrokerConnection connection = connectionProvider.createBrokerConnection("test-connection", Map.of());
+
+ assertNotNull(connection);
+ assertEquals("test-connection", connection.getName());
+ }
+
+ @Test
+ void testCreateBrokerConnection_MissingAmqpUri() {
+ when(endpointView.getAmqpUriKey()).thenReturn(Optional.empty());
+
+ ServiceException exception = assertThrows(ServiceException.class, () -> {
+ connectionProvider.createBrokerConnection("test-connection", Map.of());
+ });
+
+ assertEquals("AMQP URI key is missing in the service binding. Please check the service binding configuration.", exception.getMessage());
+ }
+
+ @Test
+ void testCreateBrokerConnection_MissingToken() throws IOException {
+ when(endpointView.getAmqpUriKey()).thenReturn(Optional.of("amqp://example.com"));
+ when(endpointView.getVpn()).thenReturn(Optional.of("test-vpn"));
+ when(tokenFetchClient.fetchToken()).thenReturn(Optional.empty());
+
+ ServiceException exception = assertThrows(ServiceException.class, () -> {
+ connectionProvider.createBrokerConnection("test-connection", Map.of());
+ });
+
+ assertEquals("AMQP URI key is missing in the service binding. Please check the service binding configuration.", exception.getMessage());
+ }
+
+ @Test
+ void testCreateBrokerConnection_TokenFetch() throws IOException {
+ when(endpointView.getAmqpUriKey()).thenReturn(Optional.of("amqp://example.com"));
+ when(endpointView.getVpn()).thenReturn(Optional.of("test-vpn"));
+ when(serviceBinding.getCredentials()).thenReturn(
+ Map.of("endpoints", Map.of("advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")),
+ "vpn", "test-vpn")
+ );
+ when(tokenFetchClient.fetchToken()).thenThrow(new IOException("IO error"));
+
+ assertDoesNotThrow(() -> {
+ connectionProvider.createBrokerConnection("test-connection", Map.of());
+ });
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/AemMessagingServiceConfigurationTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java
similarity index 98%
rename from cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/AemMessagingServiceConfigurationTest.java
rename to cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java
index 4f0bbc2..851118e 100644
--- a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/AemMessagingServiceConfigurationTest.java
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java
@@ -1,4 +1,4 @@
-package com.sap.cds.feature.messaging.aem;
+package com.sap.cds.feature.messaging.aem.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
From f20dc5d4adb86b4b2317abc9b0b0ce5013cb9028 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Mon, 17 Mar 2025 16:22:32 +0100
Subject: [PATCH 05/32] Cleanedup test case
---
.../aem/jms/AemMessagingConnectionProviderTest.java | 8 --------
1 file changed, 8 deletions(-)
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
index 0e28dfa..0cf3805 100644
--- a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
@@ -47,8 +47,6 @@ void setUp() {
@Test
void testCreateBrokerConnection_Success() throws Exception {
- when(endpointView.getAmqpUriKey()).thenReturn(Optional.of("amqp://example.com"));
- when(endpointView.getVpn()).thenReturn(Optional.of("test-vpn"));
when(serviceBinding.getCredentials()).thenReturn(
Map.of("endpoints", Map.of("advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")),
"vpn", "test-vpn")
@@ -63,8 +61,6 @@ void testCreateBrokerConnection_Success() throws Exception {
@Test
void testCreateBrokerConnection_MissingAmqpUri() {
- when(endpointView.getAmqpUriKey()).thenReturn(Optional.empty());
-
ServiceException exception = assertThrows(ServiceException.class, () -> {
connectionProvider.createBrokerConnection("test-connection", Map.of());
});
@@ -74,8 +70,6 @@ void testCreateBrokerConnection_MissingAmqpUri() {
@Test
void testCreateBrokerConnection_MissingToken() throws IOException {
- when(endpointView.getAmqpUriKey()).thenReturn(Optional.of("amqp://example.com"));
- when(endpointView.getVpn()).thenReturn(Optional.of("test-vpn"));
when(tokenFetchClient.fetchToken()).thenReturn(Optional.empty());
ServiceException exception = assertThrows(ServiceException.class, () -> {
@@ -87,8 +81,6 @@ void testCreateBrokerConnection_MissingToken() throws IOException {
@Test
void testCreateBrokerConnection_TokenFetch() throws IOException {
- when(endpointView.getAmqpUriKey()).thenReturn(Optional.of("amqp://example.com"));
- when(endpointView.getVpn()).thenReturn(Optional.of("test-vpn"));
when(serviceBinding.getCredentials()).thenReturn(
Map.of("endpoints", Map.of("advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")),
"vpn", "test-vpn")
From 2cbe6e39163a1a6ee733e62d2330aed30df6c24b Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Mon, 17 Mar 2025 16:53:09 +0100
Subject: [PATCH 06/32] Using cds-services 3.8.0 for now
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 6e4a86b..c4c4ad8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,7 +28,7 @@
${java.version}
UTF-8
- 3.9.0-SNAPSHOT
+ 3.8.0
8.7.3
2.7.0
From 79857b96b0d3ddfce584aa4bb1127d11ec06a508 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Tue, 18 Mar 2025 08:05:18 +0100
Subject: [PATCH 07/32] Added CHANGELOG.md, updated README.md and version
---
CHANGELOG.md | 32 ++++++++++++++++++++++++++++++++
README.md | 21 ++++++++++++++++++++-
pom.xml | 2 +-
3 files changed, 53 insertions(+), 2 deletions(-)
create mode 100644 CHANGELOG.md
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..52094f4
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,32 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## Version 3.9.0 - tbd
+
+### Added
+
+- Initial release of the plugin.
+
+### Changed
+
+
+### Deprecated
+
+
+### Removed
+
+
+### Fixed
+
+
+### Security
+
+
+## Diffs
+
+[unreleased]: https://github.com/cap-java/cds-feature-advanced-event-meshg/compare/v3.9.0...HEAD
+[3.9.0]: https://github.com/cap-java/cds-feature-advanced-event-mesh/releases/tag/v3.9.0
diff --git a/README.md b/README.md
index 5906896..015ee2c 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,32 @@ CDS plugin providing integration with SAP Integration Suite, advanced event mesh
## Requirements and Setup
-*Insert a short description what is required to get your project running...*
+See [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell?impl-variant=java) on how to jumpstart your development and grow as you go with SAP Cloud Application Programming Model.
+
+### SAP Integration Suite, advanced event mesh
+
+For details on how to use SAP Integration Suite, advanced event mesh, please see the [SAP Integration Suite, advanced event mesh Service Guide](https://TODO).
+
+### CDS Plugin
+
+The usage of CAP Java plugins is described in the [CAP Java Documentation](https://cap.cloud.sap/docs/java/building-plugins#reference-the-new-cds-model-in-an-existing-cap-java-project). Following this documentation this plugin needs to be referenced in the `srv/pom.xml` of a CAP Java project:
+
+```xml
+
+ com.sap.cds
+ cds-feature-advanced-event-mesh
+ ${latest-version}
+
+```
+
+The latest version can be found in the [changelog](./CHANGELOG.md) or in the [Maven Central Repository](https://central.sonatype.com/artifact/com.sap.cds/cds-feature-advanced-event-mesh/versions).
## Support, Feedback, Contributing
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-java/cds-feature-advanced-event-mesh/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
## Security / Disclosure
+
If you find any bug that may be a security problem, please follow our instructions at [in our security policy](https://github.com/cap-java/cds-feature-advanced-event-mesh/security/policy) on how to report it. Please do not create GitHub issues for security-related doubts or problems.
## Code of Conduct
diff --git a/pom.xml b/pom.xml
index c4c4ad8..1e3717f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
- 4.1.0-SNAPSHOT
+ 3.9.0-SNAPSHOT
17
${java.version}
UTF-8
From 5d2688981c2ef32319d0a1554f48e0529701c7a0 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Tue, 18 Mar 2025 09:28:22 +0100
Subject: [PATCH 08/32] Removed unused constant.
---
.../messaging/aem/jms/AemMessagingConnectionProvider.java | 1 -
1 file changed, 1 deletion(-)
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
index d1f6953..c3253db 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
@@ -24,7 +24,6 @@ public class AemMessagingConnectionProvider extends BrokerConnectionProvider {
private static final Logger logger = LoggerFactory.getLogger(AemMessagingConnectionProvider.class);
- private static final String AEM_TOKEN_FETCH_DESTINATION = "x-cap-aem-token-fetch-destination";
private static final String SASL_MECHANISM_URI_PARAMETER = "/?amqp.saslMechanisms=XOAUTH2";
private final ServiceBinding binding;
From 9c24da747713ed94c6bfda6c9f1fb980e7dc5568 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Tue, 18 Mar 2025 14:36:51 +0100
Subject: [PATCH 09/32] Updated Link to AEM Docs
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 015ee2c..5c85b52 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ See [Getting Started](https://cap.cloud.sap/docs/get-started/in-a-nutshell?impl-
### SAP Integration Suite, advanced event mesh
-For details on how to use SAP Integration Suite, advanced event mesh, please see the [SAP Integration Suite, advanced event mesh Service Guide](https://TODO).
+For details on how to use SAP Integration Suite, advanced event mesh, please see the [SAP Integration Suite, Advanced Event Mesh Service Guide](https://help.sap.com/docs/sap-integration-suite/sap-integration-suite-advanced-event-mesh-728c56cd25854f0fad611eb26ae17152/what-is-sap-integration-suite-advanced-event-mesh?state=DRAFT).
### CDS Plugin
From 3fbdcea886777334d8a32ddfb3bb4456e624a16e Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Wed, 19 Mar 2025 14:34:13 +0100
Subject: [PATCH 10/32] Implemented token fetch using CloudSDK
---
.../aem/client/binding/AemEndpointView.java | 4 +-
.../client/binding/AemTokenFetchClient.java | 95 -------------------
.../jms/AemMessagingConnectionProvider.java | 53 ++++++++---
.../client/binding/AemEndpointViewTest.java | 8 +-
.../binding/AemTokenFetchClientTest.java | 72 --------------
.../AemMessagingConnectionProviderTest.java | 94 ------------------
6 files changed, 46 insertions(+), 280 deletions(-)
delete mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
delete mode 100644 cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java
delete mode 100644 cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java
index 3a5d4d8..d567d61 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointView.java
@@ -41,7 +41,7 @@ public Optional getUri() {
* @return an {@link Optional} containing the AMQP URI if present, otherwise an
* empty {@link Optional}.
*/
- public Optional getAmqpUriKey() {
+ public Optional getAmqpUri() {
return Optional.ofNullable((String) getAemEndpoint().get(AMQP_URI_KEY));
}
@@ -75,4 +75,4 @@ private Map getAemEndpoint() {
}
}
-}
\ No newline at end of file
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
deleted file mode 100644
index b7bb6df..0000000
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClient.java
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.sap.cds.feature.messaging.aem.client.binding;
-
-import java.io.IOException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.util.Optional;
-
-import org.apache.http.client.ClientProtocolException;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.client.methods.HttpPost;
-import org.apache.http.client.methods.HttpRequestBase;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.apache.http.impl.client.HttpClients;
-import org.apache.http.util.EntityUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.sap.cds.services.ServiceException;
-import com.sap.cloud.security.xsuaa.http.HttpHeaders;
-import com.sap.cloud.security.xsuaa.http.MediaType;
-
-public class AemTokenFetchClient {
-
- private static final Logger logger = LoggerFactory.getLogger(AemTokenFetchClient.class);
-
- private final AemAuthorizationServiceView authorizationServiceView;
- private final ObjectMapper mapper = new ObjectMapper();
-
- public AemTokenFetchClient(AemAuthorizationServiceView authorizationServiceView) {
- this.authorizationServiceView = authorizationServiceView;
- }
-
- public Optional fetchToken() throws IOException {
- String requestBody = String.format(
- "grant_type=client_credentials&client_id=%s&client_secret=%s",
- URLEncoder.encode(this.authorizationServiceView.getClientId().get(), StandardCharsets.UTF_8),
- URLEncoder.encode(this.authorizationServiceView.getClientSecret().get(), StandardCharsets.UTF_8));
-
- HttpPost post = new HttpPost(this.authorizationServiceView.getTokenEndpoint().get());
- post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED.value());
- post.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.value());
-
- post.setEntity(new StringEntity(requestBody, StandardCharsets.UTF_8));
-
- try(CloseableHttpResponse response = handleRequest(post)) {
- JsonNode jsonNode = handleJsonResponse(response);
- JsonNode accessTokenNode = jsonNode.get("access_token");
-
- return Optional.ofNullable(accessTokenNode).map(JsonNode::asText);
- } finally {
- post.releaseConnection();
- }
- }
-
- private CloseableHttpResponse handleRequest(HttpRequestBase request) throws IOException {
- try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
- return httpClient.execute(request);
- } catch (ClientProtocolException e) {
- throw new ServiceException(e.getMessage(), e);
- }
- }
-
- private JsonNode handleJsonResponse(CloseableHttpResponse response) throws IOException {
- try (CloseableHttpResponse resp = response){
- int code = resp.getStatusLine().getStatusCode();
-
- logger.debug("Responded with status code '{}'", code);
-
- if (code >= 200 && code <= 207) {
- String contentType = MediaType.APPLICATION_JSON.value();
-
- if (resp.getEntity() != null) {
- if (resp.getEntity().getContentType() != null) {
- contentType = resp.getEntity().getContentType().getValue();
- }
- if (contentType.startsWith(MediaType.APPLICATION_JSON.value())) {
- String jsonData = EntityUtils.toString(resp.getEntity());
- return mapper.readValue(jsonData, JsonNode.class);
- } else {
- throw new IOException("Unexpected response format: Expected JSON but found '" + contentType + "'");
- }
- } else {
- return mapper.readValue("{}", JsonNode.class);
- }
- } else {
- String reason = resp.getStatusLine().getReasonPhrase();
- throw new ServiceException("Unexpected request HTTP response (" + code + ") " + reason);
- }
- }
- }
-
-}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
index c3253db..34aba5e 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProvider.java
@@ -1,22 +1,27 @@
package com.sap.cds.feature.messaging.aem.jms;
-import java.io.IOException;
import java.net.URI;
import java.util.Map;
+import java.util.Optional;
import java.util.function.BiFunction;
import org.apache.qpid.jms.JmsConnectionExtensions;
import org.apache.qpid.jms.JmsConnectionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-
-import com.sap.cds.feature.messaging.aem.client.binding.AemAuthorizationServiceView;
import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView;
-import com.sap.cds.feature.messaging.aem.client.binding.AemTokenFetchClient;
+import com.sap.cds.feature.messaging.aem.client.binding.AemOauth2PropertySupplier;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.messaging.jms.BrokerConnection;
import com.sap.cds.services.messaging.jms.BrokerConnectionProvider;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+import com.sap.cloud.sdk.cloudplatform.connectivity.AuthenticationType;
+import com.sap.cloud.sdk.cloudplatform.connectivity.Destination;
+import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2DestinationBuilder;
+import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2PropertySupplier;
+import com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+import com.sap.cloud.security.xsuaa.http.HttpHeaders;
import jakarta.jms.Connection;
@@ -28,13 +33,19 @@ public class AemMessagingConnectionProvider extends BrokerConnectionProvider {
private final ServiceBinding binding;
private final AemEndpointView endpointView;
- private final AemTokenFetchClient tokenFetchClient;
+ private final Destination destination;
public AemMessagingConnectionProvider(ServiceBinding binding) {
super(binding.getName().get());
this.binding = binding;
this.endpointView = new AemEndpointView(binding);
- this.tokenFetchClient = new AemTokenFetchClient(new AemAuthorizationServiceView(binding));
+ OAuth2PropertySupplier supplier = new AemOauth2PropertySupplier(ServiceBindingDestinationOptions.forService(binding).build());
+ this.destination = OAuth2DestinationBuilder
+ .forTargetUrl(this.endpointView.getAmqpUri().get())
+ .withTokenEndpoint(supplier.getTokenUri().toString())
+ .withClient(supplier.getClientIdentity(), OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT)
+ .authenticationType(AuthenticationType.OAUTH2_CLIENT_CREDENTIALS)
+ .build();
}
@Override
@@ -42,20 +53,16 @@ protected BrokerConnection createBrokerConnection(String name, Map new ServiceException(
+ String amqpUri = this.endpointView.getAmqpUri().orElseThrow(() -> new ServiceException(
"AMQP URI key is missing in the service binding. Please check the service binding configuration."));
amqpUri = amqpUri + SASL_MECHANISM_URI_PARAMETER;
final BiFunction tokenExtension = new BiFunction<>() {
@Override
public Object apply(final Connection connection, final URI uri) {
- try {
- String token = tokenFetchClient.fetchToken().orElseThrow(() -> new ServiceException("Token is missing"));
+ String token = fetchToken().orElseThrow(() -> new ServiceException("Token is missing"));
- return token;
- } catch (IOException e) {
- throw new ServiceException(e);
- }
+ return token;
}
};
@@ -68,4 +75,24 @@ public Object apply(final Connection connection, final URI uri) {
return new BrokerConnection(name, factory);
}
+ private Optional fetchToken() {
+ Optional token = this.destination.asHttp()
+ .getHeaders()
+ .stream()
+ .filter(h -> h.getName().equals(HttpHeaders.AUTHORIZATION))
+ .findFirst()
+ .map(h -> getToken(h.getValue()));
+ return token;
+ }
+
+ private String getToken(String value) {
+ String token = null;
+ if (value != null) {
+ String[] parts = value.split(" ");
+ token = parts.length > 1 ? parts[1] : null;
+ }
+
+ return token;
+ }
+
}
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java
index 7b5487f..55f4676 100644
--- a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java
+++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemEndpointViewTest.java
@@ -49,7 +49,7 @@ void testGetUri_NotPresent() {
}
@Test
- void testGetAmqpUriKey() {
+ void testGetAmqpUri() {
Map credentials = Map.of(
"endpoints", Map.of(
"advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")
@@ -57,16 +57,16 @@ void testGetAmqpUriKey() {
);
when(serviceBinding.getCredentials()).thenReturn(credentials);
- Optional amqpUri = endpointView.getAmqpUriKey();
+ Optional amqpUri = endpointView.getAmqpUri();
assertTrue(amqpUri.isPresent());
assertEquals("amqp://example.com", amqpUri.get());
}
@Test
- void testGetAmqpUriKey_NotPresent() {
+ void testGetAmqpUri_NotPresent() {
when(serviceBinding.getCredentials()).thenReturn(Map.of());
- Optional amqpUri = endpointView.getAmqpUriKey();
+ Optional amqpUri = endpointView.getAmqpUri();
assertFalse(amqpUri.isPresent());
}
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java
deleted file mode 100644
index f545944..0000000
--- a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemTokenFetchClientTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.sap.cds.feature.messaging.aem.client.binding;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
-import java.io.IOException;
-import java.util.Optional;
-
-import org.apache.http.StatusLine;
-import org.apache.http.client.methods.CloseableHttpResponse;
-import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import com.sap.cds.services.ServiceException;
-
-public class AemTokenFetchClientTest {
-
- @Mock
- private AemAuthorizationServiceView authorizationServiceView;
-
- @Mock
- private CloseableHttpClient httpClient;
-
- @Mock
- private CloseableHttpResponse response;
-
- @Mock
- private StatusLine statusLine;
-
- private AemTokenFetchClient tokenFetchClient;
-
- @BeforeEach
- void setUp() {
- MockitoAnnotations.openMocks(this);
- tokenFetchClient = new AemTokenFetchClient(authorizationServiceView);
- }
-
- @Test
- void testFetchToken_Failure() throws IOException {
- when(authorizationServiceView.getClientId()).thenReturn(Optional.of("test-client-id"));
- when(authorizationServiceView.getClientSecret()).thenReturn(Optional.of("test-client-secret"));
- when(authorizationServiceView.getTokenEndpoint()).thenReturn(Optional.of("http://example.com/token"));
-
- when(this.statusLine.getStatusCode()).thenReturn(400);
- when(response.getStatusLine()).thenReturn(this.statusLine);
-
- when(response.getStatusLine().getStatusCode()).thenReturn(400);
- when(response.getStatusLine().getReasonPhrase()).thenReturn("Bad Request");
- when(httpClient.execute(any())).thenReturn(response);
-
- assertThrows(ServiceException.class, () -> tokenFetchClient.fetchToken());
- }
-
- @Test
- void testFetchToken_InvalidJsonResponse() throws IOException {
- when(authorizationServiceView.getClientId()).thenReturn(Optional.of("test-client-id"));
- when(authorizationServiceView.getClientSecret()).thenReturn(Optional.of("test-client-secret"));
- when(authorizationServiceView.getTokenEndpoint()).thenReturn(Optional.of("http://example.com/token"));
-
- String invalidJsonResponse = "Invalid JSON";
- when(this.statusLine.getStatusCode()).thenReturn(200);
- when(response.getStatusLine()).thenReturn(this.statusLine);
-
- when(response.getEntity()).thenReturn(new StringEntity(invalidJsonResponse));
- when(httpClient.execute(any())).thenReturn(response);
-
- assertThrows(ServiceException.class, () -> tokenFetchClient.fetchToken());
- }
-}
diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
deleted file mode 100644
index 0cf3805..0000000
--- a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/jms/AemMessagingConnectionProviderTest.java
+++ /dev/null
@@ -1,94 +0,0 @@
-package com.sap.cds.feature.messaging.aem.jms;
-
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
-import java.io.IOException;
-import java.net.URI;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.BiFunction;
-
-import org.apache.qpid.jms.JmsConnectionFactory;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import com.sap.cds.feature.messaging.aem.client.binding.AemAuthorizationServiceView;
-import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView;
-import com.sap.cds.feature.messaging.aem.client.binding.AemTokenFetchClient;
-import com.sap.cds.services.ServiceException;
-import com.sap.cds.services.messaging.jms.BrokerConnection;
-import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
-
-import jakarta.jms.Connection;
-
-public class AemMessagingConnectionProviderTest {
-
- @Mock
- private ServiceBinding serviceBinding;
-
- @Mock
- private AemEndpointView endpointView;
-
- @Mock
- private AemTokenFetchClient tokenFetchClient;
-
- private AemMessagingConnectionProvider connectionProvider;
-
- @BeforeEach
- void setUp() {
- MockitoAnnotations.openMocks(this);
- when(serviceBinding.getName()).thenReturn(Optional.of("test-binding"));
-
- connectionProvider = new AemMessagingConnectionProvider(serviceBinding);
- }
-
- @Test
- void testCreateBrokerConnection_Success() throws Exception {
- when(serviceBinding.getCredentials()).thenReturn(
- Map.of("endpoints", Map.of("advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")),
- "vpn", "test-vpn")
- );
- when(tokenFetchClient.fetchToken()).thenReturn(Optional.of("test-token"));
-
- BrokerConnection connection = connectionProvider.createBrokerConnection("test-connection", Map.of());
-
- assertNotNull(connection);
- assertEquals("test-connection", connection.getName());
- }
-
- @Test
- void testCreateBrokerConnection_MissingAmqpUri() {
- ServiceException exception = assertThrows(ServiceException.class, () -> {
- connectionProvider.createBrokerConnection("test-connection", Map.of());
- });
-
- assertEquals("AMQP URI key is missing in the service binding. Please check the service binding configuration.", exception.getMessage());
- }
-
- @Test
- void testCreateBrokerConnection_MissingToken() throws IOException {
- when(tokenFetchClient.fetchToken()).thenReturn(Optional.empty());
-
- ServiceException exception = assertThrows(ServiceException.class, () -> {
- connectionProvider.createBrokerConnection("test-connection", Map.of());
- });
-
- assertEquals("AMQP URI key is missing in the service binding. Please check the service binding configuration.", exception.getMessage());
- }
-
- @Test
- void testCreateBrokerConnection_TokenFetch() throws IOException {
- when(serviceBinding.getCredentials()).thenReturn(
- Map.of("endpoints", Map.of("advanced-event-mesh", Map.of("amqp_uri", "amqp://example.com")),
- "vpn", "test-vpn")
- );
- when(tokenFetchClient.fetchToken()).thenThrow(new IOException("IO error"));
-
- assertDoesNotThrow(() -> {
- connectionProvider.createBrokerConnection("test-connection", Map.of());
- });
- }
-}
From d07c5c6f7cb125254dd2ca151b568386d8809302 Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Wed, 19 Mar 2025 14:35:05 +0100
Subject: [PATCH 11/32] Set version to 0.1.0-SNAPSHOT
---
pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pom.xml b/pom.xml
index 1e3717f..85c1538 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
- 3.9.0-SNAPSHOT
+ 0.1.0-SNAPSHOT
17
${java.version}
UTF-8
From b03397401c3f934e16ef112dcbb56fec7c0de4fc Mon Sep 17 00:00:00 2001
From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com>
Date: Thu, 20 Mar 2025 07:53:57 +0100
Subject: [PATCH 12/32] Got rid of custom token fetch; implemented check for
SAP-provisioned Solace Message Broker.
---
.../aem/client/AemManagementClient.java | 15 ++-
.../aem/client/AemValidationClient.java | 26 +++++
.../aem/client/binding/AemClientIdentity.java | 15 +++
... AemManagementOauth2PropertySupplier.java} | 21 +---
.../AemValidationOAuth2PropertySupplier.java | 98 +++++++++++++++++++
.../jms/AemMessagingConnectionProvider.java | 5 +-
.../aem/service/AemMessagingService.java | 32 +++++-
.../AemMessagingServiceConfiguration.java | 20 ++--
.../AemAuthorizationServiceViewTest.java | 6 +-
.../client/binding/AemEndpointViewTest.java | 6 +-
.../AemMessagingServiceConfigurationTest.java | 1 -
11 files changed, 210 insertions(+), 35 deletions(-)
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemValidationClient.java
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemClientIdentity.java
rename cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/{AemOauth2PropertySupplier.java => AemManagementOauth2PropertySupplier.java} (82%)
create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemValidationOAuth2PropertySupplier.java
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
index 9075afa..cd6042d 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
@@ -13,8 +13,11 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.sap.cds.feature.messaging.aem.client.binding.AemAuthorizationServiceView;
+import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView;
import com.sap.cds.integration.cloudsdk.rest.client.JsonRestClient;
import com.sap.cds.integration.cloudsdk.rest.client.JsonRestClientResponseException;
+import com.sap.cds.services.ServiceException;
import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
@@ -33,17 +36,23 @@ public class AemManagementClient extends JsonRestClient {
public static final String ATTR_QUEUE_NAME = "queueName";
public static final String ATTR_SUBSCRIPTION_TOPIC = "subscriptionTopic";
- private final ServiceBinding binding;
+ private final AemAuthorizationServiceView authorizationServiceView;
+ private final AemEndpointView endpointView;
private final String vpn;
private final String owner;
public AemManagementClient(ServiceBinding binding) {
super(ServiceBindingDestinationOptions.forService(binding).build());
- this.binding = binding;
+ this.authorizationServiceView = new AemAuthorizationServiceView(binding);
+ this.endpointView = new AemEndpointView(binding);
this.vpn = getVpn();
this.owner = getOwner();
}
+ public String getEndpoint() {
+ return this.endpointView.getUri().orElseThrow(() -> new ServiceException("Management endpoint not available in binding"));
+ }
+
public void removeQueue(String queue) throws IOException {
logger.debug("Removing queue {}", queue);
@@ -139,7 +148,7 @@ public boolean isTopicSubscribed(JsonNode jsonNode, String queueName, String top
}
private String getVpn() {
- return (String) this.binding.getCredentials().get("vpn");
+ return this.endpointView.getVpn().get();
}
private String getOwner() {
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemValidationClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemValidationClient.java
new file mode 100644
index 0000000..c3493d1
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemValidationClient.java
@@ -0,0 +1,26 @@
+package com.sap.cds.feature.messaging.aem.client;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+
+import com.sap.cds.integration.cloudsdk.rest.client.JsonRestClient;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+
+public class AemValidationClient extends JsonRestClient {
+
+ public AemValidationClient(ServiceBinding binding) {
+ super(ServiceBindingDestinationOptions.forService(binding).build());
+ }
+
+ public void validate(String managementUri) throws IOException, URISyntaxException {
+ URI uri = new URI(managementUri);
+ String payload = this.mapper.writeValueAsString(Map.of("hostName", uri.getHost()));
+
+ // The response is not used, only the status code is relevant. If there is a status code not equal to 200,
+ // an exception is thrown which means that the validation failed.
+ postRequest("", payload, Map.of());
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemClientIdentity.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemClientIdentity.java
new file mode 100644
index 0000000..431f0dd
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemClientIdentity.java
@@ -0,0 +1,15 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+record AemClientIdentity(String clientId, String clientSecret)
+ implements com.sap.cloud.security.config.ClientIdentity {
+
+ @Override
+ public String getId() {
+ return this.clientId;
+ }
+
+ @Override
+ public String getSecret() {
+ return this.clientSecret;
+ }
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemOauth2PropertySupplier.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemManagementOauth2PropertySupplier.java
similarity index 82%
rename from cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemOauth2PropertySupplier.java
rename to cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemManagementOauth2PropertySupplier.java
index efcfa3e..114a1d6 100644
--- a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemOauth2PropertySupplier.java
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemManagementOauth2PropertySupplier.java
@@ -14,7 +14,7 @@
import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2ServiceBindingDestinationLoader;
import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
-public class AemOauth2PropertySupplier implements OAuth2PropertySupplier {
+public class AemManagementOauth2PropertySupplier implements OAuth2PropertySupplier {
private static boolean initialized = false;
@@ -27,12 +27,12 @@ public static synchronized void initialize() {
OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(
options -> ServiceBindingUtils.matches(options.getServiceBinding(),
AemMessagingServiceConfiguration.BINDING_AEM_LABEL),
- AemOauth2PropertySupplier::new);
+ AemManagementOauth2PropertySupplier::new);
initialized = true;
}
}
- public AemOauth2PropertySupplier(@Nonnull ServiceBindingDestinationOptions options) {
+ public AemManagementOauth2PropertySupplier(@Nonnull ServiceBindingDestinationOptions options) {
this.binding = options.getServiceBinding();
this.authorizationServiceView = new AemAuthorizationServiceView(binding);
this.endpointView = new AemEndpointView(binding);
@@ -69,7 +69,7 @@ public URI getTokenUri() {
@Nonnull
@Override
public com.sap.cloud.security.config.ClientIdentity getClientIdentity() {
- return new ClientIdentity(this.authorizationServiceView.getClientId().get(),
+ return new AemClientIdentity(this.authorizationServiceView.getClientId().get(),
this.authorizationServiceView.getClientSecret().get());
}
@@ -85,17 +85,4 @@ private boolean areOAuth2ParametersPresent(ServiceBinding binding) {
&& this.authorizationServiceView.getClientSecret().isPresent();
}
- private record ClientIdentity(String clientId, String clientSecret)
- implements com.sap.cloud.security.config.ClientIdentity {
-
- @Override
- public String getId() {
- return this.clientId;
- }
-
- @Override
- public String getSecret() {
- return this.clientSecret;
- }
- }
}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemValidationOAuth2PropertySupplier.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemValidationOAuth2PropertySupplier.java
new file mode 100644
index 0000000..64df617
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemValidationOAuth2PropertySupplier.java
@@ -0,0 +1,98 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.Optional;
+
+import com.sap.cds.feature.messaging.aem.service.AemMessagingServiceConfiguration;
+import com.sap.cds.services.ServiceException;
+import com.sap.cds.services.utils.environment.ServiceBindingUtils;
+import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2PropertySupplier;
+import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2ServiceBindingDestinationLoader;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+import com.sap.cloud.security.config.ClientIdentity;
+
+public class AemValidationOAuth2PropertySupplier implements OAuth2PropertySupplier {
+
+ private static boolean initialized = false;
+
+ private final CredentialsView credentialsView;
+
+ public static synchronized void initialize() {
+ if (!initialized) {
+ OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(
+ options -> ServiceBindingUtils.matches(options.getServiceBinding(),
+ AemMessagingServiceConfiguration.BINDING_AEM_VALIDATION_LABEL),
+ AemValidationOAuth2PropertySupplier::new);
+ initialized = true;
+ }
+ }
+
+ protected AemValidationOAuth2PropertySupplier(ServiceBindingDestinationOptions options) {
+ this.credentialsView = new CredentialsView(options.getServiceBinding().getCredentials());
+ }
+
+ @Override
+ public boolean isOAuth2Binding() {
+ return this.credentialsView.getServiceUri().isPresent()
+ && this.credentialsView.getTokenUri().isPresent()
+ && this.credentialsView.getClientId().isPresent()
+ && this.credentialsView.getClientSecret().isPresent();
+ }
+
+ @Override
+ public URI getServiceUri() {
+ try {
+ return new URI(this.credentialsView.getServiceUri().get());
+ } catch (URISyntaxException e) {
+ throw new ServiceException("Invalid AEM Validation Service URI.", e);
+ }
+ }
+
+ @Override
+ public URI getTokenUri() {
+ String uri = this.credentialsView.getTokenUri().get();
+
+ try {
+ return new URI(uri);
+ } catch (URISyntaxException e) {
+ throw new ServiceException("Invalid AEM Validation Service token endpoint URI.", e);
+ }
+ }
+
+ @Override
+ public ClientIdentity getClientIdentity() {
+ return new AemClientIdentity(this.credentialsView.getClientId().get(), this.credentialsView.getClientSecret().get());
+ }
+
+ private record CredentialsView(Map credentials) {
+
+ public Optional getServiceUri() {
+ return getHandshake().map(handshake -> (String) handshake.get("uri"));
+ }
+
+ public Optional getTokenUri() {
+ return getOAuth2().map(oa2 -> (String) oa2.get("tokenendpoint"));
+ }
+
+ public Optional getClientId() {
+ return getOAuth2().map(oa2 -> (String) oa2.get("clientid"));
+ }
+
+ public Optional getClientSecret() {
+ return getOAuth2().map(oa2 -> (String) oa2.get("clientsecret"));
+ }
+
+ @SuppressWarnings("unchecked")
+ private Optional