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/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..06f5c56
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,28 @@
+# 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 0.1.0 - tbd
+
+### Added
+
+- Initial release of the plugin.
+
+### Changed
+
+
+### Deprecated
+
+
+### Removed
+
+
+### Fixed
+
+
+### Security
+
+
diff --git a/README.md b/README.md
index 5906896..9dae2a2 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://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
+
+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/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..8858c76
--- /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/#advanced-event-mesh-plugin
+
+
+
+
+ com.sap.cds
+ cds-services-messaging
+
+
+
+ com.sap.cds
+ cds-services-utils
+
+
+
+ com.sap.cloud.sdk.cloudplatform
+ connectivity-apache-httpclient4
+
+
+
+ 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.3
+
+
+ BRANCH
+ COVEREDRATIO
+ 0.2
+
+
+ COMPLEXITY
+ COVEREDRATIO
+ 0.3
+
+
+ CLASS
+ MISSEDCOUNT
+ 4
+
+
+
+
+
+
+
+
+
+
+ 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..8567c72
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemManagementClient.java
@@ -0,0 +1,139 @@
+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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView;
+import com.sap.cds.services.ServiceException;
+import com.sap.cloud.environment.servicebinding.api.ServiceBinding;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+
+public class AemManagementClient extends RestClient {
+ private static final Logger logger = LoggerFactory.getLogger(AemManagementClient.class);
+
+ private static final String API_BASE = "/SEMP/v2/config/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";
+
+ private static final String ATTR_EGRESS_ENABLED = "egressEnabled";
+ private static final String ATTR_INGRESS_ENABLED = "ingressEnabled";
+ public static final String ATTR_DEAD_MSG_QUEUE = "deadMsgQueue";
+ private static final String ATTR_PERMISSION = "permission";
+ private static final String ATTR_QUEUE_NAME = "queueName";
+ private static final String ATTR_SUBSCRIPTION_TOPIC = "subscriptionTopic";
+ private static final String VAL_CONSUME = "consume";
+
+ private final AemEndpointView endpointView;
+ private final String vpn;
+
+ public AemManagementClient(ServiceBinding binding) {
+ super(ServiceBindingDestinationOptions.forService(binding).build());
+ this.endpointView = new AemEndpointView(binding);
+ this.vpn = getVpn();
+ }
+
+ 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);
+
+ 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 (Exception e) {
+ 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_PERMISSION, VAL_CONSUME);
+ attributes.put(ATTR_INGRESS_ENABLED, true);
+ attributes.put(ATTR_EGRESS_ENABLED, true);
+
+ postRequest(uri(API_QUEUE, this.vpn), attributes);
+ }
+ }
+
+ 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);
+ postRequest(uri(API_QUEUE_NAME_SUBSCRIPTION, this.vpn, queue), attributes);
+ }
+ }
+
+ 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 this.endpointView.getVpn().get();
+ }
+
+}
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..65c282e
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/AemValidationClient.java
@@ -0,0 +1,25 @@
+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.cloud.environment.servicebinding.api.ServiceBinding;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+
+public class AemValidationClient extends RestClient {
+
+ 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/RestClient.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/RestClient.java
new file mode 100644
index 0000000..792bb4c
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/RestClient.java
@@ -0,0 +1,117 @@
+package com.sap.cds.feature.messaging.aem.client;
+
+import static org.apache.http.entity.ContentType.APPLICATION_JSON;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.http.HttpHeaders;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.entity.StringEntity;
+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.sdk.cloudplatform.connectivity.HttpClientAccessor;
+import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions;
+
+class RestClient {
+
+ private static final Logger logger = LoggerFactory.getLogger(RestClient.class);
+
+ private final HttpDestination destination;
+
+ protected final ObjectMapper mapper = new ObjectMapper();
+
+ RestClient(ServiceBindingDestinationOptions bindingDestinationOptions) {
+ this.destination = ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(bindingDestinationOptions);
+ }
+
+ public JsonNode getRequest(String path) throws IOException {
+ HttpGet get = new HttpGet(path);
+ try {
+ return handleJsonResponse(handleRequest(get));
+ } finally {
+ get.releaseConnection();
+ }
+ }
+
+ public JsonNode postRequest(String path, Map data) throws IOException {
+ String strData = mapper.writer().writeValueAsString(data);
+ return postRequest(path, strData, null);
+ }
+
+ public JsonNode postRequest(String path, String data, Map headers) throws IOException {
+ HttpPost post = new HttpPost(path);
+
+ if (headers != null) {
+ headers.forEach((k, v) -> post.setHeader(k, v.toString()));
+ }
+
+ post.setHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON.toString());
+ post.setHeader(HttpHeaders.ACCEPT, APPLICATION_JSON.toString());
+
+ if (data != null) {
+ post.setEntity(new StringEntity(data, APPLICATION_JSON));
+ }
+ try {
+ return handleJsonResponse(handleRequest(post));
+ } finally {
+ post.releaseConnection();
+ }
+ }
+
+ public JsonNode deleteRequest(String path) throws IOException {
+ HttpDelete del = new HttpDelete(path);
+ try {
+ return handleJsonResponse(handleRequest(del));
+ } finally {
+ del.releaseConnection();
+ }
+ }
+
+ private CloseableHttpResponse handleRequest(HttpRequestBase request) throws IOException {
+ HttpClient httpClient = HttpClientAccessor.getHttpClient(destination);
+ return (CloseableHttpResponse) httpClient.execute(request);
+ }
+
+ 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 = APPLICATION_JSON.toString();
+
+ if (resp.getEntity() != null) {
+ if (resp.getEntity().getContentType() != null) {
+ contentType = resp.getEntity().getContentType().getValue();
+ }
+ if (APPLICATION_JSON.toString().startsWith(contentType)) {
+ 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/client/binding/AemAuthenticationServiceView.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthenticationServiceView.java
new file mode 100644
index 0000000..4b25941
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthenticationServiceView.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;
+
+/**
+ * AemAuthenticationServiceView provides a view of the authentication service
+ * credentials from a given ServiceBinding.
+ */
+public class AemAuthenticationServiceView {
+ 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 AemAuthenticationServiceView(ServiceBinding binding) {
+ this.binding = binding;
+ }
+
+ @SuppressWarnings("unchecked")
+ private Map getAuthenticationService() {
+ 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.getAuthenticationService().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.getAuthenticationService().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.getAuthenticationService().get(CLIENTSECRET_KEY));
+ }
+}
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/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..d567d61
--- /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 getAmqpUri() {
+ 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();
+ }
+ }
+
+}
diff --git a/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemManagementOauth2PropertySupplier.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemManagementOauth2PropertySupplier.java
new file mode 100644
index 0000000..7a6bea3
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemManagementOauth2PropertySupplier.java
@@ -0,0 +1,87 @@
+package com.sap.cds.feature.messaging.aem.client.binding;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.annotation.Nonnull;
+
+import com.sap.cds.feature.messaging.aem.service.AemMessagingServiceConfiguration;
+import com.sap.cds.services.ServiceException;
+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 AemManagementOauth2PropertySupplier implements OAuth2PropertySupplier {
+
+ private static boolean initialized = false;
+
+ private final ServiceBinding binding;
+ private final AemAuthenticationServiceView authenticationServiceView;
+ private final AemEndpointView endpointView;
+
+ public static synchronized void initialize() {
+ if (!initialized) {
+ OAuth2ServiceBindingDestinationLoader.registerPropertySupplier(
+ options -> (options.getServiceBinding().getName().isPresent() &&
+ AemMessagingServiceConfiguration.BINDING_AEM_LABEL.equals(options.getServiceBinding().getName().get()))
+ || options.getServiceBinding().getTags().contains(AemMessagingServiceConfiguration.BINDING_AEM_LABEL),
+ AemManagementOauth2PropertySupplier::new);
+ initialized = true;
+ }
+ }
+
+ public AemManagementOauth2PropertySupplier(@Nonnull ServiceBindingDestinationOptions options) {
+ this.binding = options.getServiceBinding();
+ this.authenticationServiceView = new AemAuthenticationServiceView(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.authenticationServiceView.getTokenEndpoint().get());
+ } catch (URISyntaxException e) {
+ throw new ServiceException(e.getMessage(), e);
+ }
+ }
+
+ @Nonnull
+ @Override
+ public com.sap.cloud.security.config.ClientIdentity getClientIdentity() {
+ return new AemClientIdentity(this.authenticationServiceView.getClientId().get(),
+ this.authenticationServiceView.getClientSecret().get());
+ }
+
+ private boolean isAemBinding(ServiceBinding binding) {
+ return (binding.getName().isPresent() &&
+ AemMessagingServiceConfiguration.BINDING_AEM_LABEL.equals(binding.getName().get()))
+ || binding.getTags().contains(AemMessagingServiceConfiguration.BINDING_AEM_LABEL);
+ }
+
+ private boolean areOAuth2ParametersPresent(ServiceBinding binding) {
+ return this.authenticationServiceView.getTokenEndpoint().isPresent()
+ && this.authenticationServiceView.getClientId().isPresent()
+ && this.authenticationServiceView.getClientSecret().isPresent();
+ }
+
+}
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..9a11974
--- /dev/null
+++ b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemValidationOAuth2PropertySupplier.java
@@ -0,0 +1,97 @@
+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