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> getHandshake() { + return Optional.ofNullable((Map) credentials.get("handshake")); + } + + @SuppressWarnings("unchecked") + private Optional> getOAuth2() { + return getHandshake().map(handshake -> (Map) handshake.get("oa2")); + } + + } +} 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 34aba5e..882da0b 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 @@ -9,8 +9,9 @@ import org.apache.qpid.jms.JmsConnectionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; + import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView; -import com.sap.cds.feature.messaging.aem.client.binding.AemOauth2PropertySupplier; +import com.sap.cds.feature.messaging.aem.client.binding.AemManagementOauth2PropertySupplier; import com.sap.cds.services.ServiceException; import com.sap.cds.services.messaging.jms.BrokerConnection; import com.sap.cds.services.messaging.jms.BrokerConnectionProvider; @@ -39,7 +40,7 @@ public AemMessagingConnectionProvider(ServiceBinding binding) { super(binding.getName().get()); this.binding = binding; this.endpointView = new AemEndpointView(binding); - OAuth2PropertySupplier supplier = new AemOauth2PropertySupplier(ServiceBindingDestinationOptions.forService(binding).build()); + OAuth2PropertySupplier supplier = new AemManagementOauth2PropertySupplier(ServiceBindingDestinationOptions.forService(binding).build()); this.destination = OAuth2DestinationBuilder .forTargetUrl(this.endpointView.getAmqpUri().get()) .withTokenEndpoint(supplier.getTokenUri().toString()) 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 2481b58..a3fb2ce 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 @@ -3,8 +3,10 @@ import static com.sap.cds.feature.messaging.aem.client.AemManagementClient.ATTR_DEAD_MSG_QUEUE; import java.io.IOException; +import java.net.URISyntaxException; import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import org.apache.qpid.jms.message.JmsBytesMessage; @@ -15,7 +17,9 @@ import org.slf4j.LoggerFactory; import com.sap.cds.feature.messaging.aem.client.AemManagementClient; +import com.sap.cds.feature.messaging.aem.client.AemValidationClient; import com.sap.cds.feature.messaging.aem.jms.AemMessagingConnectionProvider; +import com.sap.cds.services.ServiceException; import com.sap.cds.services.environment.CdsProperties.Messaging.MessagingServiceConfig; import com.sap.cds.services.messaging.TopicMessageEventContext; import com.sap.cds.services.messaging.jms.BrokerConnection; @@ -32,14 +36,20 @@ public class AemMessagingService extends AbstractMessagingService { private final AemMessagingConnectionProvider connectionProvider; private final AemManagementClient managementClient; + private final Optional validationBinding; private volatile BrokerConnection connection; + private volatile Boolean aemBrokerValidated = false; + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + protected AemMessagingService(ServiceBinding binding, Optional validationBinding, + MessagingServiceConfig serviceConfig, AemMessagingConnectionProvider connectionProvider, CdsRuntime runtime) { - protected AemMessagingService(ServiceBinding binding, MessagingServiceConfig serviceConfig, AemMessagingConnectionProvider connectionProvider, CdsRuntime runtime) { super(serviceConfig, runtime); this.connectionProvider = connectionProvider; this.managementClient = new AemManagementClient(binding); + this.validationBinding = validationBinding; } @Override @@ -98,6 +108,7 @@ protected void registerQueueListener(String queue, MessagingBrokerQueueListener @Override protected void emitTopicMessage(String topic, TopicMessageEventContext messageEventContext) { + this.validate(this.managementClient.getEndpoint()); this.connection.emitTopicMessage("topic://" + topic, messageEventContext); } @@ -116,4 +127,23 @@ private String getMessageTopic(Message message) { } return null; } + + private void validate(String endpoint) { + synchronized (this.aemBrokerValidated) { + if (!this.aemBrokerValidated) { + ServiceBinding binding = this.validationBinding.orElseThrow(() -> new ServiceException("No binding for AEM Validation Service found.")); + AemValidationClient validationClient = new AemValidationClient(binding); + + try { + validationClient.validate(endpoint); + synchronized (this.aemBrokerValidated) { + this.aemBrokerValidated = true; + } + } catch (IOException | URISyntaxException e) { + throw new ServiceException("Failed to validate the AEM endpoint.", e); + } + } + } + } + } 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 index ff8411f..02e5f36 100644 --- 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 @@ -4,11 +4,13 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; 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.client.binding.AemManagementOauth2PropertySupplier; +import com.sap.cds.feature.messaging.aem.client.binding.AemValidationOAuth2PropertySupplier; 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; @@ -25,16 +27,20 @@ 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 BINDING_AEM_VALIDATION_LABEL = "aem-validation-service"; public static final String AEM_KIND = "advanced-event-mesh"; @Override public void services(CdsRuntimeConfigurer configurer) { - AemOauth2PropertySupplier.initialize(); + AemManagementOauth2PropertySupplier.initialize(); + AemValidationOAuth2PropertySupplier.initialize(); Messaging config = configurer.getCdsRuntime().getEnvironment().getCdsProperties().getMessaging(); List bindings = configurer.getCdsRuntime().getEnvironment().getServiceBindings() .filter(binding -> ServiceBindingUtils.matches(binding, BINDING_AEM_LABEL)) .toList(); + Optional validationBinding = configurer.getCdsRuntime().getEnvironment().getServiceBindings() + .filter(binding -> ServiceBindingUtils.matches(binding, BINDING_AEM_VALIDATION_LABEL)).findFirst(); if (bindings.isEmpty()) { logger.info("No service bindings with name '{}' found", BINDING_AEM_LABEL); @@ -62,7 +68,7 @@ public void services(CdsRuntimeConfigurer configurer) { serviceConfigs.forEach(serviceConfig -> { if (serviceConfig.isEnabled()) { // register the service - configurer.service(createMessagingService(binding, sharedConnectionProvider, serviceConfig, + configurer.service(createMessagingService(binding, validationBinding, sharedConnectionProvider, serviceConfig, configurer.getCdsRuntime(), Objects.equals(AEM_KIND, serviceConfig.getKind()))); } }); @@ -84,7 +90,7 @@ public void services(CdsRuntimeConfigurer configurer) { if (serviceConfig.isEnabled() && serviceConfigs.stream() .noneMatch(c -> c.getName().equals(serviceConfig.getName()))) { // register the service - configurer.service(createMessagingService(binding, sharedConnectionProvider, serviceConfig, + configurer.service(createMessagingService(binding, validationBinding, sharedConnectionProvider, serviceConfig, configurer.getCdsRuntime(), Objects.equals(AEM_KIND, serviceConfig.getKind()))); } }); @@ -97,7 +103,7 @@ public void services(CdsRuntimeConfigurer configurer) { 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.service(createMessagingService(binding, validationBinding, sharedConnectionProvider, defConfig, configurer.getCdsRuntime(), false)); } else { logger.warn( @@ -112,10 +118,10 @@ public void services(CdsRuntimeConfigurer configurer) { } } - private MessagingService createMessagingService(ServiceBinding binding, + private MessagingService createMessagingService(ServiceBinding binding, Optional validationBinding, AemMessagingConnectionProvider sharedConnectionProvider, MessagingServiceConfig serviceConfig, CdsRuntime runtime, boolean forceShared) { - MessagingService service = new AemMessagingService(binding, serviceConfig, sharedConnectionProvider, runtime); + MessagingService service = new AemMessagingService(binding, validationBinding, serviceConfig, sharedConnectionProvider, runtime); logger.debug("Created messaging service '{}' for binding '{}'", serviceConfig.getName(), binding.getName().get()); 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 index 0128c3d..24d5307 100644 --- 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 @@ -1,7 +1,9 @@ package com.sap.cds.feature.messaging.aem.client.binding; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; import java.util.Map; import java.util.Optional; 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 55f4676..d5cbe0f 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 @@ -1,7 +1,9 @@ package com.sap.cds.feature.messaging.aem.client.binding; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.when; import java.util.Map; import java.util.Optional; diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java index 851118e..75f3205 100644 --- a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java +++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java @@ -8,7 +8,6 @@ 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; From 7248a8d5f0dbe4765ffc57c2fd34696a5df9e434 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Thu, 20 Mar 2025 07:59:30 +0100 Subject: [PATCH 13/32] adjusted test data --- .../src/test/resources/default-env.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 index 0bda1a3..04b0cb6 100644 --- a/cds-feature-advanced-event-mesh/src/test/resources/default-env.json +++ b/cds-feature-advanced-event-mesh/src/test/resources/default-env.json @@ -12,16 +12,16 @@ "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=" + "tokenendpoint": "https://my.host.com/oauth2/token", + "clientid": "myid", + "clientsecret": "mysecret" }, "endpoints": { "advanced-event-mesh": { - "uri": "https://mr-connection-i90693rt5n6.messaging.solace.cloud:943/SEMP/v2/config", + "uri": "https://my.other.host.com: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" + "amqp_uri": "amqps://broker.host.com:5671", + "smf_uri": "wss://broker.host.com:443" } } }, From 605bdeb8f7c4c14771a015c65d55fc73187842be Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Thu, 20 Mar 2025 08:41:25 +0100 Subject: [PATCH 14/32] removed synchronization as its not required here --- .../feature/messaging/aem/service/AemMessagingService.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 a3fb2ce..108575c 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 @@ -129,21 +129,17 @@ private String getMessageTopic(Message message) { } private void validate(String endpoint) { - synchronized (this.aemBrokerValidated) { if (!this.aemBrokerValidated) { ServiceBinding binding = this.validationBinding.orElseThrow(() -> new ServiceException("No binding for AEM Validation Service found.")); AemValidationClient validationClient = new AemValidationClient(binding); try { validationClient.validate(endpoint); - synchronized (this.aemBrokerValidated) { - this.aemBrokerValidated = true; - } + this.aemBrokerValidated = true; } catch (IOException | URISyntaxException e) { throw new ServiceException("Failed to validate the AEM endpoint.", e); } } - } } } From 918693fdd29a08b4b372aca994f66f8dd1eff4a8 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:12:36 +0100 Subject: [PATCH 15/32] Updated repository link --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 85c1538..9399404 100644 --- a/pom.xml +++ b/pom.xml @@ -251,8 +251,8 @@ 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 + scm:git:git@github.com:cap-java/cds-feature-advanced-event-mesh.git + scm:git:git@github.com:cap-java/cds-feature-advanced-event-mesh.git From 743d768dc67d28904fd4582080d4f6a90c390c7d Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:59:39 +0100 Subject: [PATCH 16/32] Updated CHANGELOG.md --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 52094f4..49d9d8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ 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 +## Version 0.1.0 - tbd ### Added @@ -28,5 +28,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 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 +- [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) From ad223c0f9181fd06f7d2a35414df9da4372daff0 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Tue, 25 Mar 2025 08:43:10 +0100 Subject: [PATCH 17/32] Changes after review --- .../aem/client/AemManagementClient.java | 6 ++--- ...java => AemAuthenticationServiceView.java} | 6 ++--- .../AemManagementOauth2PropertySupplier.java | 16 +++++++------- ... => AemAuthenticationServiceViewTest.java} | 22 +++++++++---------- 4 files changed, 25 insertions(+), 25 deletions(-) rename cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/{AemAuthorizationServiceView.java => AemAuthenticationServiceView.java} (95%) rename cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/{AemAuthorizationServiceViewTest.java => AemAuthenticationServiceViewTest.java} (77%) 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 cd6042d..c8c4840 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,7 +13,7 @@ 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.AemAuthenticationServiceView; 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; @@ -36,14 +36,14 @@ public class AemManagementClient extends JsonRestClient { public static final String ATTR_QUEUE_NAME = "queueName"; public static final String ATTR_SUBSCRIPTION_TOPIC = "subscriptionTopic"; - private final AemAuthorizationServiceView authorizationServiceView; + private final AemAuthenticationServiceView authenticationServiceView; private final AemEndpointView endpointView; private final String vpn; private final String owner; public AemManagementClient(ServiceBinding binding) { super(ServiceBindingDestinationOptions.forService(binding).build()); - this.authorizationServiceView = new AemAuthorizationServiceView(binding); + this.authenticationServiceView = new AemAuthenticationServiceView(binding); this.endpointView = new AemEndpointView(binding); this.vpn = getVpn(); this.owner = getOwner(); 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/AemAuthenticationServiceView.java similarity index 95% rename from cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceView.java rename to cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthenticationServiceView.java index bc4358b..1ba4f7d 100644 --- 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/AemAuthenticationServiceView.java @@ -9,7 +9,7 @@ * AemAuthorizationServiceView provides a view of the authorization service * credentials from a given ServiceBinding. */ -public class AemAuthorizationServiceView { +public class AemAuthenticationServiceView { private static final String CLIENTSECRET_KEY = "clientsecret"; private static final String CLIENTID_KEY = "clientid"; private static final String TOKENENDPOINT_KEY = "tokenendpoint"; @@ -17,7 +17,7 @@ public class AemAuthorizationServiceView { private final ServiceBinding binding; - public AemAuthorizationServiceView(ServiceBinding binding) { + public AemAuthenticationServiceView(ServiceBinding binding) { this.binding = binding; } @@ -69,4 +69,4 @@ public Optional getClientId() { 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/AemManagementOauth2PropertySupplier.java b/cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemManagementOauth2PropertySupplier.java index 114a1d6..fb5f54e 100644 --- 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 @@ -19,7 +19,7 @@ public class AemManagementOauth2PropertySupplier implements OAuth2PropertySuppli private static boolean initialized = false; private final ServiceBinding binding; - private final AemAuthorizationServiceView authorizationServiceView; + private final AemAuthenticationServiceView authenticationServiceView; private final AemEndpointView endpointView; public static synchronized void initialize() { @@ -34,7 +34,7 @@ public static synchronized void initialize() { public AemManagementOauth2PropertySupplier(@Nonnull ServiceBindingDestinationOptions options) { this.binding = options.getServiceBinding(); - this.authorizationServiceView = new AemAuthorizationServiceView(binding); + this.authenticationServiceView = new AemAuthenticationServiceView(binding); this.endpointView = new AemEndpointView(binding); } @@ -60,7 +60,7 @@ public URI getServiceUri() { @Override public URI getTokenUri() { try { - return new URI(this.authorizationServiceView.getTokenEndpoint().get()); + return new URI(this.authenticationServiceView.getTokenEndpoint().get()); } catch (URISyntaxException e) { throw new ServiceException(e.getMessage(), e); } @@ -69,8 +69,8 @@ public URI getTokenUri() { @Nonnull @Override public com.sap.cloud.security.config.ClientIdentity getClientIdentity() { - return new AemClientIdentity(this.authorizationServiceView.getClientId().get(), - this.authorizationServiceView.getClientSecret().get()); + return new AemClientIdentity(this.authenticationServiceView.getClientId().get(), + this.authenticationServiceView.getClientSecret().get()); } private boolean isAemBinding(ServiceBinding binding) { @@ -80,9 +80,9 @@ private boolean isAemBinding(ServiceBinding binding) { } private boolean areOAuth2ParametersPresent(ServiceBinding binding) { - return this.authorizationServiceView.getTokenEndpoint().isPresent() - && this.authorizationServiceView.getClientId().isPresent() - && this.authorizationServiceView.getClientSecret().isPresent(); + return this.authenticationServiceView.getTokenEndpoint().isPresent() + && this.authenticationServiceView.getClientId().isPresent() + && this.authenticationServiceView.getClientSecret().isPresent(); } } 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/AemAuthenticationServiceViewTest.java similarity index 77% rename from cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthorizationServiceViewTest.java rename to cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthenticationServiceViewTest.java index 24d5307..6aef224 100644 --- 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/AemAuthenticationServiceViewTest.java @@ -15,17 +15,17 @@ import com.sap.cloud.environment.servicebinding.api.ServiceBinding; -public class AemAuthorizationServiceViewTest { +public class AemAuthenticationServiceViewTest { @Mock private ServiceBinding serviceBinding; - private AemAuthorizationServiceView authorizationServiceView; + private AemAuthenticationServiceView authenticationServiceView; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - authorizationServiceView = new AemAuthorizationServiceView(serviceBinding); + authenticationServiceView = new AemAuthenticationServiceView(serviceBinding); } @Test @@ -39,14 +39,14 @@ void testIsTokenEndpointPresent() { ); when(serviceBinding.getCredentials()).thenReturn(credentials); - assertTrue(authorizationServiceView.isTokenEndpointPresent()); + assertTrue(authenticationServiceView.isTokenEndpointPresent()); } @Test void testIsTokenEndpointPresent_NotPresent() { when(serviceBinding.getCredentials()).thenReturn(Map.of()); - assertFalse(authorizationServiceView.isTokenEndpointPresent()); + assertFalse(authenticationServiceView.isTokenEndpointPresent()); } @Test @@ -56,7 +56,7 @@ void testGetTokenEndpoint() { ); when(serviceBinding.getCredentials()).thenReturn(credentials); - Optional tokenEndpoint = authorizationServiceView.getTokenEndpoint(); + Optional tokenEndpoint = authenticationServiceView.getTokenEndpoint(); assertTrue(tokenEndpoint.isPresent()); assertEquals("http://example.com/token", tokenEndpoint.get()); } @@ -65,7 +65,7 @@ void testGetTokenEndpoint() { void testGetTokenEndpoint_NotPresent() { when(serviceBinding.getCredentials()).thenReturn(Map.of()); - Optional tokenEndpoint = authorizationServiceView.getTokenEndpoint(); + Optional tokenEndpoint = authenticationServiceView.getTokenEndpoint(); assertFalse(tokenEndpoint.isPresent()); } @@ -76,7 +76,7 @@ void testGetClientId() { ); when(serviceBinding.getCredentials()).thenReturn(credentials); - Optional clientId = authorizationServiceView.getClientId(); + Optional clientId = authenticationServiceView.getClientId(); assertTrue(clientId.isPresent()); assertEquals("test-client-id", clientId.get()); } @@ -85,7 +85,7 @@ void testGetClientId() { void testGetClientId_NotPresent() { when(serviceBinding.getCredentials()).thenReturn(Map.of()); - Optional clientId = authorizationServiceView.getClientId(); + Optional clientId = authenticationServiceView.getClientId(); assertFalse(clientId.isPresent()); } @@ -96,7 +96,7 @@ void testGetClientSecret() { ); when(serviceBinding.getCredentials()).thenReturn(credentials); - Optional clientSecret = authorizationServiceView.getClientSecret(); + Optional clientSecret = authenticationServiceView.getClientSecret(); assertTrue(clientSecret.isPresent()); assertEquals("test-client-secret", clientSecret.get()); } @@ -105,7 +105,7 @@ void testGetClientSecret() { void testGetClientSecret_NotPresent() { when(serviceBinding.getCredentials()).thenReturn(Map.of()); - Optional clientSecret = authorizationServiceView.getClientSecret(); + Optional clientSecret = authenticationServiceView.getClientSecret(); assertFalse(clientSecret.isPresent()); } } From cebdd5491e164952e9d0a1a064f649226a342a82 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:20:44 +0100 Subject: [PATCH 18/32] Replaced the JsonRestClient with an own RestClient --- cds-feature-advanced-event-mesh/pom.xml | 10 +- .../aem/client/AemManagementClient.java | 14 +-- .../aem/client/AemValidationClient.java | 3 +- .../messaging/aem/client/RestClient.java | 117 ++++++++++++++++++ .../jms/AemMessagingConnectionProvider.java | 34 ++--- .../AemMessagingServiceConfiguration.java | 2 +- 6 files changed, 143 insertions(+), 37 deletions(-) create mode 100644 cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/RestClient.java diff --git a/cds-feature-advanced-event-mesh/pom.xml b/cds-feature-advanced-event-mesh/pom.xml index ef5a126..8858c76 100644 --- a/cds-feature-advanced-event-mesh/pom.xml +++ b/cds-feature-advanced-event-mesh/pom.xml @@ -23,12 +23,12 @@ com.sap.cds - cds-integration-cloud-sdk + cds-services-utils - com.sap.cds - cds-services-utils + com.sap.cloud.sdk.cloudplatform + connectivity-apache-httpclient4 @@ -227,7 +227,7 @@ BRANCH COVEREDRATIO - 0.3 + 0.2 COMPLEXITY @@ -237,7 +237,7 @@ CLASS MISSEDCOUNT - 3 + 4 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 c8c4840..2d5acd5 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,15 +13,12 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.sap.cds.feature.messaging.aem.client.binding.AemAuthenticationServiceView; 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; -public class AemManagementClient extends JsonRestClient { +public class AemManagementClient extends RestClient { private static final Logger logger = LoggerFactory.getLogger(AemManagementClient.class); private static final String API_BASE = "/msgVpns/%s"; @@ -36,14 +33,12 @@ public class AemManagementClient extends JsonRestClient { public static final String ATTR_QUEUE_NAME = "queueName"; public static final String ATTR_SUBSCRIPTION_TOPIC = "subscriptionTopic"; - private final AemAuthenticationServiceView authenticationServiceView; private final AemEndpointView endpointView; private final String vpn; private final String owner; public AemManagementClient(ServiceBinding binding) { super(ServiceBindingDestinationOptions.forService(binding).build()); - this.authenticationServiceView = new AemAuthenticationServiceView(binding); this.endpointView = new AemEndpointView(binding); this.vpn = getVpn(); this.owner = getOwner(); @@ -70,12 +65,7 @@ public JsonNode getQueue(String name) throws IOException { 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; - } - + } catch (Exception e) { logger.error("Failed to retrieve information for queue {}", name, e); return null; } 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 index c3493d1..65c282e 100644 --- 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 @@ -5,11 +5,10 @@ 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 class AemValidationClient extends RestClient { public AemValidationClient(ServiceBinding binding) { super(ServiceBindingDestinationOptions.forService(binding).build()); 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..0b1d6d6 --- /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 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.ContentType; +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; +import com.sap.cloud.security.xsuaa.http.MediaType; + +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, JsonNode 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, MediaType.APPLICATION_JSON.toString()); + post.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.toString()); + + if (data != null) { + post.setEntity(new StringEntity(data, ContentType.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 = 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 882da0b..44e2c2a 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 @@ -11,16 +11,15 @@ import org.slf4j.LoggerFactory; import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView; -import com.sap.cds.feature.messaging.aem.client.binding.AemManagementOauth2PropertySupplier; 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.DefaultHttpDestination; 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.DestinationProperty; import com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf; +import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; import com.sap.cloud.security.xsuaa.http.HttpHeaders; @@ -33,19 +32,24 @@ public class AemMessagingConnectionProvider extends BrokerConnectionProvider { private static final String SASL_MECHANISM_URI_PARAMETER = "/?amqp.saslMechanisms=XOAUTH2"; private final ServiceBinding binding; - private final AemEndpointView endpointView; private final Destination destination; public AemMessagingConnectionProvider(ServiceBinding binding) { super(binding.getName().get()); this.binding = binding; - this.endpointView = new AemEndpointView(binding); - OAuth2PropertySupplier supplier = new AemManagementOauth2PropertySupplier(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) + + AemEndpointView endpointView = new AemEndpointView(binding); + String amqpUri = 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; + + ServiceBindingDestinationOptions options = ServiceBindingDestinationOptions.forService(binding). + onBehalfOf(OnBehalfOf.TECHNICAL_USER_CURRENT_TENANT).build(); + + this.destination = DefaultHttpDestination.fromDestination( + ServiceBindingDestinationLoader.defaultLoaderChain().getDestination(options)) + .uri(amqpUri) + .property("vpn", endpointView.getVpn().get()) .build(); } @@ -54,10 +58,6 @@ protected BrokerConnection createBrokerConnection(String name, Map 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) { @@ -69,7 +69,7 @@ public Object apply(final Connection connection, final URI uri) { 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); + JmsConnectionFactory factory = new JmsConnectionFactory(destination.get("vpn", String.class).get(), "token", destination.get(DestinationProperty.URI).get()); factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), tokenExtension); 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 index 02e5f36..cb91f3e 100644 --- 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 @@ -28,7 +28,7 @@ public class AemMessagingServiceConfiguration implements CdsRuntimeConfiguration public static final String BINDING_AEM_LABEL = "advanced-event-mesh"; public static final String BINDING_AEM_VALIDATION_LABEL = "aem-validation-service"; - public static final String AEM_KIND = "advanced-event-mesh"; + public static final String AEM_KIND = "aem"; @Override public void services(CdsRuntimeConfigurer configurer) { From 18c59b07e57c2cf40ad34790e459bfa1ffda4a57 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:21:10 +0100 Subject: [PATCH 19/32] Optimized Imports --- .../cds/feature/messaging/aem/client/AemManagementClient.java | 1 - 1 file changed, 1 deletion(-) 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 2d5acd5..7eecb5a 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 @@ -7,7 +7,6 @@ import java.util.HashMap; import java.util.Map; -import org.apache.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From ae833af66bc3cc1262b276d9fb5088e5a824aa9a Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:41:18 +0100 Subject: [PATCH 20/32] Setting the permission to consume instead of passing an owner --- .../feature/messaging/aem/client/AemManagementClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 7eecb5a..f52c960 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 @@ -28,9 +28,10 @@ public class AemManagementClient extends RestClient { 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_PERMISSION = "permission"; public static final String ATTR_QUEUE_NAME = "queueName"; public static final String ATTR_SUBSCRIPTION_TOPIC = "subscriptionTopic"; + public static final String VAL_CONSUME = "consume"; private final AemEndpointView endpointView; private final String vpn; @@ -80,7 +81,7 @@ public void createQueue(String name, Map properties) throws IOEx Map attributes = new HashMap<>(properties); attributes.put(ATTR_QUEUE_NAME, name); - attributes.put(ATTR_OWNER, this.owner); + attributes.put(ATTR_PERMISSION, VAL_CONSUME); attributes.put(ATTR_INGRESS_ENABLED, true); attributes.put(ATTR_EGRESS_ENABLED, true); From f674b605acd0021a5d8b21a0b28e361e83ac64e2 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Wed, 26 Mar 2025 16:52:45 +0100 Subject: [PATCH 21/32] Changes after code review --- .../AemValidationOAuth2PropertySupplier.java | 5 +++-- .../jms/AemMessagingConnectionProvider.java | 2 +- .../aem/service/AemMessagingService.java | 7 +++---- .../AemMessagingServiceConfiguration.java | 19 +++++++++++-------- 4 files changed, 18 insertions(+), 15 deletions(-) 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 index 64df617..fcbb22f 100644 --- 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 @@ -8,12 +8,12 @@ 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.DefaultOAuth2PropertySupplier; 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 { +public class AemValidationOAuth2PropertySupplier extends DefaultOAuth2PropertySupplier { private static boolean initialized = false; @@ -30,6 +30,7 @@ public static synchronized void initialize() { } protected AemValidationOAuth2PropertySupplier(ServiceBindingDestinationOptions options) { + super(options); this.credentialsView = new CredentialsView(options.getServiceBinding().getCredentials()); } 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 44e2c2a..0cac792 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 @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.function.BiFunction; +import org.apache.http.HttpHeaders; import org.apache.qpid.jms.JmsConnectionExtensions; import org.apache.qpid.jms.JmsConnectionFactory; import org.slf4j.Logger; @@ -21,7 +22,6 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; -import com.sap.cloud.security.xsuaa.http.HttpHeaders; import jakarta.jms.Connection; 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 108575c..679da3f 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 @@ -36,13 +36,13 @@ public class AemMessagingService extends AbstractMessagingService { private final AemMessagingConnectionProvider connectionProvider; private final AemManagementClient managementClient; - private final Optional validationBinding; + private final ServiceBinding validationBinding; private volatile BrokerConnection connection; private volatile Boolean aemBrokerValidated = false; @SuppressWarnings("OptionalUsedAsFieldOrParameterType") - protected AemMessagingService(ServiceBinding binding, Optional validationBinding, + protected AemMessagingService(ServiceBinding binding, ServiceBinding validationBinding, MessagingServiceConfig serviceConfig, AemMessagingConnectionProvider connectionProvider, CdsRuntime runtime) { super(serviceConfig, runtime); @@ -130,8 +130,7 @@ private String getMessageTopic(Message message) { private void validate(String endpoint) { if (!this.aemBrokerValidated) { - ServiceBinding binding = this.validationBinding.orElseThrow(() -> new ServiceException("No binding for AEM Validation Service found.")); - AemValidationClient validationClient = new AemValidationClient(binding); + AemValidationClient validationClient = new AemValidationClient(this.validationBinding); try { validationClient.validate(endpoint); 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 index cb91f3e..8e6e6ac 100644 --- 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 @@ -9,16 +9,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Strings; import com.sap.cds.feature.messaging.aem.client.binding.AemManagementOauth2PropertySupplier; import com.sap.cds.feature.messaging.aem.client.binding.AemValidationOAuth2PropertySupplier; import com.sap.cds.feature.messaging.aem.jms.AemMessagingConnectionProvider; +import com.sap.cds.services.ServiceException; 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; @@ -69,7 +70,7 @@ public void services(CdsRuntimeConfigurer configurer) { if (serviceConfig.isEnabled()) { // register the service configurer.service(createMessagingService(binding, validationBinding, sharedConnectionProvider, serviceConfig, - configurer.getCdsRuntime(), Objects.equals(AEM_KIND, serviceConfig.getKind()))); + configurer.getCdsRuntime())); } }); } @@ -91,7 +92,7 @@ public void services(CdsRuntimeConfigurer configurer) { .noneMatch(c -> c.getName().equals(serviceConfig.getName()))) { // register the service configurer.service(createMessagingService(binding, validationBinding, sharedConnectionProvider, serviceConfig, - configurer.getCdsRuntime(), Objects.equals(AEM_KIND, serviceConfig.getKind()))); + configurer.getCdsRuntime())); } }); } @@ -101,10 +102,11 @@ public void services(CdsRuntimeConfigurer configurer) { // otherwise create default service instance for the binding MessagingServiceConfig defConfig = config.getService(binding.getName().get()); - if (StringUtils.isEmpty(defConfig.getBinding()) && StringUtils.isEmpty(defConfig.getKind())) { + + if (Strings.isNullOrEmpty(defConfig.getBinding()) && Strings.isNullOrEmpty(defConfig.getKind())) { // register the service configurer.service(createMessagingService(binding, validationBinding, sharedConnectionProvider, defConfig, - configurer.getCdsRuntime(), false)); + configurer.getCdsRuntime())); } else { logger.warn( "Could not create service for binding '{}': A configuration with the same name is already defined for another kind or binding.", @@ -119,9 +121,10 @@ public void services(CdsRuntimeConfigurer configurer) { } private MessagingService createMessagingService(ServiceBinding binding, Optional validationBinding, - AemMessagingConnectionProvider sharedConnectionProvider, MessagingServiceConfig serviceConfig, - CdsRuntime runtime, boolean forceShared) { - MessagingService service = new AemMessagingService(binding, validationBinding, serviceConfig, sharedConnectionProvider, runtime); + AemMessagingConnectionProvider sharedConnectionProvider, MessagingServiceConfig serviceConfig, CdsRuntime runtime) { + + ServiceBinding valBnd = validationBinding.orElseThrow(() -> new ServiceException("No binding for AEM Validation Service found.")); + MessagingService service = new AemMessagingService(binding, valBnd, serviceConfig, sharedConnectionProvider, runtime); logger.debug("Created messaging service '{}' for binding '{}'", serviceConfig.getName(), binding.getName().get()); From 1bb1bf9dc47b4bd0df95bd256d2e3ca59116a4db Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:14:27 +0100 Subject: [PATCH 22/32] Update README.md Co-authored-by: Stefan Henke --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5c85b52..9dae2a2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ CDS plugin providing integration with SAP Integration Suite, advanced event mesh 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 +### 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). From d3f5c9793286bd6ee1b0717e66ed90ae9ba06696 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Thu, 27 Mar 2025 11:33:18 +0100 Subject: [PATCH 23/32] Changes after code review --- CHANGELOG.md | 4 ---- .../aem/client/AemManagementClient.java | 20 +++++++++---------- .../messaging/aem/client/RestClient.java | 16 +++++++-------- .../AemManagementOauth2PropertySupplier.java | 4 +++- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49d9d8e..06f5c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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/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 f52c960..11f9f1c 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 @@ -25,13 +25,13 @@ public class AemManagementClient extends RestClient { 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_PERMISSION = "permission"; - public static final String ATTR_QUEUE_NAME = "queueName"; - public static final String ATTR_SUBSCRIPTION_TOPIC = "subscriptionTopic"; - public static final String VAL_CONSUME = "consume"; + 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; @@ -85,8 +85,7 @@ public void createQueue(String name, Map properties) throws IOEx 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); + postRequest(uri(API_QUEUE, this.vpn), attributes); } } @@ -104,8 +103,7 @@ public void createQueueSubscription(String queue, String topic) throws IOExcepti 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); + postRequest(uri(API_QUEUE_NAME_SUBSCRIPTION, this.vpn, queue), attributes); } } 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 index 0b1d6d6..4fd949c 100644 --- 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 @@ -1,5 +1,7 @@ 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; @@ -10,7 +12,6 @@ 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.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; @@ -23,7 +24,6 @@ import com.sap.cloud.sdk.cloudplatform.connectivity.HttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationLoader; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; -import com.sap.cloud.security.xsuaa.http.MediaType; class RestClient { @@ -46,7 +46,7 @@ public JsonNode getRequest(String path) throws IOException { } } - public JsonNode postRequest(String path, JsonNode data) throws IOException { + public JsonNode postRequest(String path, Map data) throws IOException { String strData = mapper.writer().writeValueAsString(data); return postRequest(path, strData, null); } @@ -58,11 +58,11 @@ public JsonNode postRequest(String path, String data, Map header headers.forEach((k, v) -> post.setHeader(k, v.toString())); } - post.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON.toString()); - post.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.toString()); + post.setHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON.toString()); + post.setHeader(HttpHeaders.ACCEPT, APPLICATION_JSON.toString()); if (data != null) { - post.setEntity(new StringEntity(data, ContentType.APPLICATION_JSON)); + post.setEntity(new StringEntity(data, APPLICATION_JSON)); } try { return handleJsonResponse(handleRequest(post)); @@ -92,13 +92,13 @@ private JsonNode handleJsonResponse(CloseableHttpResponse response) throws IOExc logger.debug("Responded with status code '{}'", code); if (code >= 200 && code <= 207) { - String contentType = MediaType.APPLICATION_JSON.value(); + String contentType = APPLICATION_JSON.toString(); if (resp.getEntity() != null) { if (resp.getEntity().getContentType() != null) { contentType = resp.getEntity().getContentType().getValue(); } - if (contentType.startsWith(MediaType.APPLICATION_JSON.value())) { + if (contentType.startsWith(APPLICATION_JSON.toString())) { String jsonData = EntityUtils.toString(resp.getEntity()); return mapper.readValue(jsonData, JsonNode.class); } else { 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 index fb5f54e..ee3b084 100644 --- 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 @@ -10,11 +10,12 @@ 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.DefaultOAuth2PropertySupplier; 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 { +public class AemManagementOauth2PropertySupplier extends DefaultOAuth2PropertySupplier { private static boolean initialized = false; @@ -33,6 +34,7 @@ public static synchronized void initialize() { } public AemManagementOauth2PropertySupplier(@Nonnull ServiceBindingDestinationOptions options) { + super(options); this.binding = options.getServiceBinding(); this.authenticationServiceView = new AemAuthenticationServiceView(binding); this.endpointView = new AemEndpointView(binding); From bcc3dcc0f98f66491474757b28de32d34bd26f52 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:10:35 +0100 Subject: [PATCH 24/32] Changes after code review --- .../com/sap/cds/feature/messaging/aem/client/RestClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 4fd949c..792bb4c 100644 --- 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 @@ -98,7 +98,7 @@ private JsonNode handleJsonResponse(CloseableHttpResponse response) throws IOExc if (resp.getEntity().getContentType() != null) { contentType = resp.getEntity().getContentType().getValue(); } - if (contentType.startsWith(APPLICATION_JSON.toString())) { + if (APPLICATION_JSON.toString().startsWith(contentType)) { String jsonData = EntityUtils.toString(resp.getEntity()); return mapper.readValue(jsonData, JsonNode.class); } else { From 4d87d31d9650ea1fe4e0ac9cd69d0f96457b848d Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:33:21 +0100 Subject: [PATCH 25/32] Changed API base URI for the management client --- .../cds/feature/messaging/aem/client/AemManagementClient.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 11f9f1c..6a270c5 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 @@ -11,7 +11,6 @@ import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; import com.sap.cds.feature.messaging.aem.client.binding.AemEndpointView; import com.sap.cds.services.ServiceException; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; @@ -20,7 +19,7 @@ public class AemManagementClient extends RestClient { private static final Logger logger = LoggerFactory.getLogger(AemManagementClient.class); - private static final String API_BASE = "/msgVpns/%s"; + 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"; From f2ac45e0b3d32a6e61fef951cf2985941d77019f Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Fri, 28 Mar 2025 09:33:44 +0100 Subject: [PATCH 26/32] optimized imports --- .../aem/client/binding/AemManagementOauth2PropertySupplier.java | 1 - .../cds/feature/messaging/aem/service/AemMessagingService.java | 1 - .../messaging/aem/service/AemMessagingServiceConfiguration.java | 1 - 3 files changed, 3 deletions(-) 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 index ee3b084..723e690 100644 --- 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 @@ -11,7 +11,6 @@ import com.sap.cds.services.utils.environment.ServiceBindingUtils; import com.sap.cloud.environment.servicebinding.api.ServiceBinding; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultOAuth2PropertySupplier; -import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2PropertySupplier; import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2ServiceBindingDestinationLoader; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; 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 679da3f..7103c54 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 @@ -6,7 +6,6 @@ import java.net.URISyntaxException; import java.util.Collections; import java.util.Map; -import java.util.Optional; import java.util.function.Consumer; import org.apache.qpid.jms.message.JmsBytesMessage; 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 index 8e6e6ac..e8d9e41 100644 --- 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 @@ -3,7 +3,6 @@ import static com.sap.cds.services.messaging.utils.MessagingOutboxUtils.outboxed; import java.util.List; -import java.util.Objects; import java.util.Optional; import org.slf4j.Logger; From 2c81c800f74ab53dfe5e702e527cce856eb01db7 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Mon, 31 Mar 2025 13:30:42 +0200 Subject: [PATCH 27/32] changed conditions for binding (user-provided) --- .../AemManagementOauth2PropertySupplier.java | 17 +++--- .../AemValidationOAuth2PropertySupplier.java | 7 ++- .../AemMessagingServiceConfiguration.java | 3 +- .../AemMessagingServiceConfigurationTest.java | 12 +++-- .../src/test/resources/default-env.json | 53 +++++++++++++++---- 5 files changed, 62 insertions(+), 30 deletions(-) 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 index 723e690..8f98c59 100644 --- 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 @@ -8,13 +8,12 @@ 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.DefaultOAuth2PropertySupplier; +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 extends DefaultOAuth2PropertySupplier { +public class AemManagementOauth2PropertySupplier implements OAuth2PropertySupplier { private static boolean initialized = false; @@ -25,15 +24,15 @@ public class AemManagementOauth2PropertySupplier extends DefaultOAuth2PropertySu public static synchronized void initialize() { if (!initialized) { OAuth2ServiceBindingDestinationLoader.registerPropertySupplier( - options -> ServiceBindingUtils.matches(options.getServiceBinding(), - AemMessagingServiceConfiguration.BINDING_AEM_LABEL), + 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) { - super(options); this.binding = options.getServiceBinding(); this.authenticationServiceView = new AemAuthenticationServiceView(binding); this.endpointView = new AemEndpointView(binding); @@ -75,9 +74,9 @@ public com.sap.cloud.security.config.ClientIdentity getClientIdentity() { } private boolean isAemBinding(ServiceBinding binding) { - Optional serviceName = binding.getName(); - - return serviceName.map(name -> name.equals(AemMessagingServiceConfiguration.BINDING_AEM_LABEL)).orElse(false); + return (binding.getName().isPresent() && + AemMessagingServiceConfiguration.BINDING_AEM_LABEL.equals(binding.getName().get())) + || binding.getTags().contains(AemMessagingServiceConfiguration.BINDING_AEM_LABEL); } private boolean areOAuth2ParametersPresent(ServiceBinding binding) { 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 index fcbb22f..48894ba 100644 --- 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 @@ -9,11 +9,12 @@ import com.sap.cds.services.ServiceException; import com.sap.cds.services.utils.environment.ServiceBindingUtils; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultOAuth2PropertySupplier; +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 extends DefaultOAuth2PropertySupplier { +public class AemValidationOAuth2PropertySupplier implements OAuth2PropertySupplier { private static boolean initialized = false; @@ -22,15 +23,13 @@ public class AemValidationOAuth2PropertySupplier extends DefaultOAuth2PropertySu public static synchronized void initialize() { if (!initialized) { OAuth2ServiceBindingDestinationLoader.registerPropertySupplier( - options -> ServiceBindingUtils.matches(options.getServiceBinding(), - AemMessagingServiceConfiguration.BINDING_AEM_VALIDATION_LABEL), + options -> ServiceBindingUtils.matches(options.getServiceBinding(), AemMessagingServiceConfiguration.BINDING_AEM_VALIDATION_LABEL), AemValidationOAuth2PropertySupplier::new); initialized = true; } } protected AemValidationOAuth2PropertySupplier(ServiceBindingDestinationOptions options) { - super(options); this.credentialsView = new CredentialsView(options.getServiceBinding().getCredentials()); } 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 index e8d9e41..365ee37 100644 --- 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 @@ -37,7 +37,8 @@ public void services(CdsRuntimeConfigurer configurer) { Messaging config = configurer.getCdsRuntime().getEnvironment().getCdsProperties().getMessaging(); List bindings = configurer.getCdsRuntime().getEnvironment().getServiceBindings() - .filter(binding -> ServiceBindingUtils.matches(binding, BINDING_AEM_LABEL)) + .filter(binding -> (binding.getName().isPresent() && BINDING_AEM_LABEL.equals(binding.getName().get())) + || binding.getTags().contains(BINDING_AEM_LABEL)) .toList(); Optional validationBinding = configurer.getCdsRuntime().getEnvironment().getServiceBindings() .filter(binding -> ServiceBindingUtils.matches(binding, BINDING_AEM_VALIDATION_LABEL)).findFirst(); diff --git a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java index 75f3205..fd907d2 100644 --- a/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java +++ b/cds-feature-advanced-event-mesh/src/test/java/com/sap/cds/feature/messaging/aem/service/AemMessagingServiceConfigurationTest.java @@ -1,5 +1,6 @@ package com.sap.cds.feature.messaging.aem.service; +import static com.sap.cds.services.outbox.OutboxService.unboxed; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -8,6 +9,7 @@ import org.junit.jupiter.api.Test; +import com.sap.cds.services.Service; import com.sap.cds.services.environment.CdsProperties; import com.sap.cds.services.environment.CdsProperties.Messaging.MessagingServiceConfig; import com.sap.cds.services.impl.environment.SimplePropertiesProvider; @@ -27,7 +29,7 @@ void testDefaultServConfiguration() { List services = configurer.getCdsRuntime().getServiceCatalog().getServices(AemMessagingService.class).toList(); assertEquals(1, services.size()); - assertEquals("advanced-event-mesh", services.get(0).getName()); + assertEquals("my-aem-instance", services.get(0).getName()); } @Test @@ -60,21 +62,21 @@ void testSingleServConfiguration() { configurer.serviceConfigurations(); configurer.eventHandlerConfigurations(); - List services = configurer.getCdsRuntime().getServiceCatalog().getServices(AemMessagingService.class).collect(Collectors.toList()); + List services = configurer.getCdsRuntime().getServiceCatalog().getServices().filter(srv -> unboxed(srv).getClass().equals(AemMessagingService.class)).toList(); assertEquals(1, services.size()); - assertEquals("cfg", services.get(0).getName()); + assertEquals("my-aem-instance", services.get(0).getName()); } @Test void testMultipleServiceConfigurations() { CdsProperties properties = new CdsProperties(); MessagingServiceConfig config1 = new MessagingServiceConfig("cfg1"); - config1.setBinding("advanced-event-mesh"); + config1.setBinding("my-aem-instance"); config1.getOutbox().setEnabled(false); MessagingServiceConfig config2 = new MessagingServiceConfig("cfg2"); - config2.setBinding("advanced-event-mesh"); + config2.setBinding("my-aem-instance"); config2.getOutbox().setEnabled(false); properties.getMessaging().getServices().put(config1.getName(), config1); 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 index 04b0cb6..5595414 100644 --- a/cds-feature-advanced-event-mesh/src/test/resources/default-env.json +++ b/cds-feature-advanced-event-mesh/src/test/resources/default-env.json @@ -1,10 +1,10 @@ { "VCAP_SERVICES": { - "advanced-event-mesh": [ + "user-provided": [ { - "label": "advanced-event-mesh", - "name": "advanced-event-mesh", - "tags": [], + "label": "user-provided", + "name": "my-aem-instance", + "tags": ["advanced-event-mesh"], "instance_guid": "515066ab-029d-4cdd-a116-a2c254633b6b", "instance_name": "advanced-event-mesh", "binding_guid": "5aec22e5-8380-47a2-99ae-1c4011cdcf88", @@ -12,22 +12,53 @@ "credentials": { "vpn": "capservice", "authentication-service": { - "tokenendpoint": "https://my.host.com/oauth2/token", - "clientid": "myid", - "clientsecret": "mysecret" + "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://my.other.host.com:943/SEMP/v2/config", - "always-requires-token": true, - "amqp_uri": "amqps://broker.host.com:5671", - "smf_uri": "wss://broker.host.com:443" + "uri": "https://mr-connection-i90693rt5n6.messaging.solace.cloud:943", + "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": [] } + ], + "aem-validation-service": [ + { + "label": "aem-validation-service", + "provider": null, + "plan": "aem-validation-service-plan", + "name": "cap-aem-validation", + "tags": [ + "eventing", + "aem", + "eventmesh" + ], + "instance_guid": "6a2c31d5-31dc-4ece-8833-eae016af64f2", + "instance_name": "cap-aem-validation", + "binding_guid": "a832ecaf-eb81-4582-a344-87354243bf07", + "binding_name": null, + "credentials": { + "xsappname": "46B0354E-C61B-11ED-AFA1-0242AC120002-6a2c31d5-31dc-4ece-8833-eae016af64f2!b191218|em-pubsub-service-broker!b38991", + "handshake": { + "oa2": { + "clientid": "sb-46B0354E-C61B-11ED-AFA1-0242AC120002-6a2c31d5-31dc-4ece-8833-eae016af64f2!b191218|em-pubsub-service-broker!b38991", + "clientsecret": "0c0251d8-c1f4-45ba-8921-075bbf986bbd$Im-xm-iWbtWC-1WDueF7UqsUxIqOUOXijWJITxFLd1Q=", + "granttype": "client_credentials", + "tokenendpoint": "https://cap-aem-nodejs.authentication.sap.hana.ondemand.com/oauth/token" + }, + "uri": "https://em-pubsub-broker.mesh.cf.sap.hana.ondemand.com/handshake" + }, + "serviceinstanceid": "6a2c31d5-31dc-4ece-8833-eae016af64f2" + }, + "syslog_drain_url": null, + "volume_mounts": [] + } ] } } From 3ca8faa1262f065d2d85c630dd206d05f62aa905 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Mon, 31 Mar 2025 14:56:10 +0200 Subject: [PATCH 28/32] optimized imports --- .../aem/client/binding/AemManagementOauth2PropertySupplier.java | 1 - .../aem/client/binding/AemValidationOAuth2PropertySupplier.java | 1 - 2 files changed, 2 deletions(-) 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 index 8f98c59..7a6bea3 100644 --- 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 @@ -2,7 +2,6 @@ import java.net.URI; import java.net.URISyntaxException; -import java.util.Optional; import javax.annotation.Nonnull; 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 index 48894ba..9a11974 100644 --- 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 @@ -8,7 +8,6 @@ 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.DefaultOAuth2PropertySupplier; import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2PropertySupplier; import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2ServiceBindingDestinationLoader; import com.sap.cloud.sdk.cloudplatform.connectivity.ServiceBindingDestinationOptions; From a41b15f92bb4e611cac4421e2517894e4df4fa11 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:42:48 +0200 Subject: [PATCH 29/32] Update cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthenticationServiceView.java Co-authored-by: Stefan Henke --- .../aem/client/binding/AemAuthenticationServiceView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 1ba4f7d..30a0b50 100644 --- 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 @@ -6,7 +6,7 @@ import com.sap.cloud.environment.servicebinding.api.ServiceBinding; /** - * AemAuthorizationServiceView provides a view of the authorization service + * AemAuthenticationServiceView provides a view of the authentication service * credentials from a given ServiceBinding. */ public class AemAuthenticationServiceView { From 4bfae849d90e3c1b952e8b7a2aa01f47f2f4ed05 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:43:01 +0200 Subject: [PATCH 30/32] Update cds-feature-advanced-event-mesh/src/main/java/com/sap/cds/feature/messaging/aem/client/binding/AemAuthenticationServiceView.java Co-authored-by: Stefan Henke --- .../aem/client/binding/AemAuthenticationServiceView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 30a0b50..7002f5f 100644 --- 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 @@ -22,7 +22,7 @@ public AemAuthenticationServiceView(ServiceBinding binding) { } @SuppressWarnings("unchecked") - private Map getAuthorizationService() { + private Map getAuthenticationService() { if (binding.getCredentials().containsKey(AUTHENTICATION_SERVICE_KEY)) { return (Map) binding.getCredentials().get(AUTHENTICATION_SERVICE_KEY); } else { From bcea3e2aab1d654ffde5ed560657aca4a86eb684 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Mon, 31 Mar 2025 15:44:15 +0200 Subject: [PATCH 31/32] renamed method --- .../aem/client/binding/AemAuthenticationServiceView.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 7002f5f..4b25941 100644 --- 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 @@ -47,7 +47,7 @@ public boolean isTokenEndpointPresent() { * otherwise an empty {@link Optional}. */ public Optional getTokenEndpoint() { - return Optional.ofNullable((String) this.getAuthorizationService().get(TOKENENDPOINT_KEY)); + return Optional.ofNullable((String) this.getAuthenticationService().get(TOKENENDPOINT_KEY)); } /** @@ -57,7 +57,7 @@ public Optional getTokenEndpoint() { * empty {@link Optional} */ public Optional getClientId() { - return Optional.ofNullable((String) this.getAuthorizationService().get(CLIENTID_KEY)); + return Optional.ofNullable((String) this.getAuthenticationService().get(CLIENTID_KEY)); } /** @@ -67,6 +67,6 @@ public Optional getClientId() { * otherwise an empty {@link Optional} */ public Optional getClientSecret() { - return Optional.ofNullable((String) this.getAuthorizationService().get(CLIENTSECRET_KEY)); + return Optional.ofNullable((String) this.getAuthenticationService().get(CLIENTSECRET_KEY)); } } From 521fd05adb1b8d4509c7418e26d08de6ec7f3313 Mon Sep 17 00:00:00 2001 From: Thomas Bonk <130759028+t-bonk@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:31:31 +0200 Subject: [PATCH 32/32] removed owner --- .../feature/messaging/aem/client/AemManagementClient.java | 6 ------ 1 file changed, 6 deletions(-) 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 6a270c5..8567c72 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 @@ -34,13 +34,11 @@ public class AemManagementClient extends RestClient { private final AemEndpointView endpointView; private final String vpn; - private final String owner; public AemManagementClient(ServiceBinding binding) { super(ServiceBindingDestinationOptions.forService(binding).build()); this.endpointView = new AemEndpointView(binding); this.vpn = getVpn(); - this.owner = getOwner(); } public String getEndpoint() { @@ -138,8 +136,4 @@ private String getVpn() { return this.endpointView.getVpn().get(); } - private String getOwner() { - return getVpn() ; - } - }