diff --git a/.github/workflows/anchore.yml b/.github/workflows/00_anchore.yml similarity index 100% rename from .github/workflows/anchore.yml rename to .github/workflows/00_anchore.yml diff --git a/.github/workflows/01_add_patch_label.yml b/.github/workflows/01_add_patch_label.yml new file mode 100644 index 00000000..41e5a825 --- /dev/null +++ b/.github/workflows/01_add_patch_label.yml @@ -0,0 +1,60 @@ +name: Add PATCH default label + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the main branch + pull_request_target: + branches: + - main + types: [ opened, reopened ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + add_patch_label: + runs-on: ubuntu-latest + name: Add default label + steps: + - name: Check user labels + id: check_user_labels + uses: actions/github-script@v6.3.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var addPatch = "true"; + // retrieve label list + let labels = await github.rest.issues.listLabelsOnIssue({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + + // verify if user have already added IGNORE-FOR-RELEASE, then skip add PATCH + // note: GitHub labels are added in .identity/03_github_environment.tf as github_issue_label resource + if (labels.data.find(label => label.name === 'ignore-for-release')){ + addPatch = "false"; + } + return addPatch; + result-encoding: string + + - name: Add PATCH label + if: ${{ steps.check_user_labels.outputs.result == 'true' }} + uses: pagopa/github-actions-template/default-label@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + label: 'patch' + + - name: Add comment + if: ${{ steps.check_user_labels.outputs.result == 'true' }} + uses: actions/github-script@v6.3.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'The default action is to increase the `PATCH` number of `SEMVER`. Set `IGNORE-FOR-RELEASE` if you want to skip `SEMVER` bump. `BREAKING-CHANGE` and `NEW-RELEASE` must be run from GH Actions section manually.' + }); diff --git a/.github/workflows/01_assignee.yml b/.github/workflows/01_assignee.yml new file mode 100644 index 00000000..06119178 --- /dev/null +++ b/.github/workflows/01_assignee.yml @@ -0,0 +1,26 @@ +name: Auto Assign + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the main branch + pull_request_target: + branches: + - main + types: [ opened, reopened ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Assign Me + # You may pin to the exact commit or the version. + uses: kentaro-m/auto-assign-action@v1.2.1 + with: + configuration-path: '.github/auto_assign.yml' diff --git a/.github/workflows/02_check_pr.yml b/.github/workflows/02_check_pr.yml new file mode 100644 index 00000000..5608117c --- /dev/null +++ b/.github/workflows/02_check_pr.yml @@ -0,0 +1,113 @@ +name: Check PR + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: [ opened, synchronize, labeled, unlabeled, reopened, edited ] + + +permissions: + pull-requests: write + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + + check_labels: + name: Check Required Labels + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + - name: Verify PR Labels + if: ${{ !contains(github.event.pull_request.labels.*.name, 'patch') && !contains(github.event.pull_request.labels.*.name, 'ignore-for-release') }} + uses: actions/github-script@v6.3.3 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + if (comment.body.includes('This pull request does not contain a valid label')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'This pull request does not contain a valid label. Please add one of the following labels: `[patch, ignore-for-release]`' + }) + core.setFailed('Missing required labels') + + + check_format: + name: Check Format + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Formatting + id: format + continue-on-error: true + uses: axel-op/googlejavaformat-action@v3 + with: + args: "--set-exit-if-changed" + + - uses: actions/github-script@v6.3.3 + if: steps.format.outcome != 'success' + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + console.log(context); + var comments = await github.rest.issues.listComments({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo + }); + for (const comment of comments.data) { + console.log(comment); + if (comment.body.includes('Comment this PR with')){ + github.rest.issues.deleteComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: comment.id + }) + } + } + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: 'Comment this PR with *update_code* to format the code. Consider to use pre-commit to format the code.' + }) + core.setFailed('Format your code.') + + + check_size: + runs-on: ubuntu-latest + name: Check Size + steps: + + - name: Dump GitHub context + run: echo $JSON + env: + JSON: ${{ toJSON(github) }} + + - name: Check PR Size + uses: pagopa/github-actions-template/check-pr-size@main + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + ignored_files: 'openapi.json' diff --git a/.github/workflows/03_code_review.yml b/.github/workflows/03_code_review.yml new file mode 100644 index 00000000..5b758663 --- /dev/null +++ b/.github/workflows/03_code_review.yml @@ -0,0 +1,44 @@ +name: Code Review + +# Controls when the workflow will run +on: + pull_request: + branches: + - main + types: + - opened + - synchronize + - reopened + push: + branches: + - main + + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +env: + PROJECT_KEY: pagopa_pagopa-wisp-converter + + +permissions: + id-token: write + contents: read + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + code-review: + name: Code Review + # The type of runner that the job will run on + runs-on: ubuntu-latest + + steps: + - name: Code Review + uses: pagopa/github-actions-template/maven-code-review@v1.10.4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + sonar_token: ${{ secrets.SONAR_TOKEN }} + project_key: ${{env.PROJECT_KEY}} + coverage_exclusions: "**/config/**,**/*Mock*,**/model/**,**/entity/*,**/util/*" + cpd_exclusions: "**/model/**,**/entity/*" + java_version: 17 \ No newline at end of file diff --git a/.github/workflows/release_deploy.yml b/.github/workflows/04_release_deploy.yml similarity index 79% rename from .github/workflows/release_deploy.yml rename to .github/workflows/04_release_deploy.yml index fb5368ab..4e665e5c 100644 --- a/.github/workflows/release_deploy.yml +++ b/.github/workflows/04_release_deploy.yml @@ -32,7 +32,6 @@ on: description: deploy beta version on AKS default: false - permissions: packages: write contents: write @@ -41,6 +40,7 @@ permissions: actions: read + # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: setup: @@ -89,7 +89,6 @@ jobs: name: Set Output run: echo "environment=${{env.ENVIRNOMENT}}" >> $GITHUB_OUTPUT - release: name: Create a New Release runs-on: ubuntu-latest @@ -99,7 +98,7 @@ jobs: steps: - name: Make Release id: release - uses: pagopa/github-actions-template/maven-release@v1.6.8 + uses: pagopa/github-actions-template/maven-release@v1.5.4 with: semver: ${{ needs.setup.outputs.semver }} github_token: ${{ secrets.BOT_TOKEN_GITHUB }} @@ -123,27 +122,7 @@ jobs: name: Deploy on AKS needs: [ setup, release, image ] if: ${{ always() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} - uses: ./.github/workflows/deploy_with_github_runner.yml + uses: ./.github/workflows/04h_deploy_with_github_runner.yml with: environment: ${{ needs.setup.outputs.environment }} secrets: inherit - - - notify: - needs: [ setup, release, deploy_aks ] - runs-on: ubuntu-latest - name: Notify - if: always() - steps: - - name: Report Status - if: ${{ needs.setup.outputs.environment == 'prod' }} - uses: ravsamhq/notify-slack-action@v2 - with: - status: ${{ needs.deploy_aks.result }} - token: ${{ secrets.GITHUB_TOKEN }} - notification_title: 'New Release on Production ${{ needs.release.outputs.version }} has {status_message}' - message_format: '{emoji} <{run_url}|{workflow}> {status_message} in <{repo_url}|{repo}>' - footer: 'Linked to <{workflow_url}| workflow file>' - icon_success: ':white_check_mark:' - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} diff --git a/.github/workflows/deploy_with_github_runner.yml b/.github/workflows/04h_deploy_with_github_runner.yml similarity index 60% rename from .github/workflows/deploy_with_github_runner.yml rename to .github/workflows/04h_deploy_with_github_runner.yml index 58fe083b..15a5f911 100644 --- a/.github/workflows/deploy_with_github_runner.yml +++ b/.github/workflows/04h_deploy_with_github_runner.yml @@ -9,7 +9,8 @@ on: type: string env: - APP_NAME: # TODO + NAMESPACE: nodo + APP_NAME: pagopawispconverter permissions: @@ -30,7 +31,7 @@ jobs: # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-create-action uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-create-action@main with: - client_id: ${{ secrets.CLIENT_ID }} + client_id: ${{ secrets.CD_CLIENT_ID }} tenant_id: ${{ secrets.TENANT_ID }} subscription_id: ${{ secrets.SUBSCRIPTION_ID }} container_app_environment_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_NAME }} @@ -47,7 +48,7 @@ jobs: uses: pagopa/github-actions-template/aks-deploy@main with: branch: ${{ github.ref_name }} - client_id: ${{ secrets.CLIENT_ID }} + client_id: ${{ secrets.CD_CLIENT_ID }} subscription_id: ${{ secrets.SUBSCRIPTION_ID }} tenant_id: ${{ secrets.TENANT_ID }} env: ${{ inputs.environment }} @@ -56,6 +57,7 @@ jobs: resource_group: ${{ vars.CLUSTER_RESOURCE_GROUP }} app_name: ${{ env.APP_NAME }} helm_upgrade_options: "--debug" + timeout: "10m0s" cleanup_runner: name: Cleanup Runner @@ -69,50 +71,9 @@ jobs: # from https://github.com/pagopa/eng-github-actions-iac-template/tree/main/azure/github-self-hosted-runner-azure-cleanup-action uses: pagopa/eng-github-actions-iac-template/azure/github-self-hosted-runner-azure-cleanup-action@0ee2f58fd46d10ac7f00bce4304b98db3dbdbe9a with: - client_id: ${{ secrets.CLIENT_ID }} + client_id: ${{ secrets.CD_CLIENT_ID }} tenant_id: ${{ secrets.TENANT_ID }} subscription_id: ${{ secrets.SUBSCRIPTION_ID }} resource_group_name: ${{ vars.CONTAINER_APP_ENVIRONMENT_RESOURCE_GROUP_NAME }} runner_name: ${{ needs.create_runner.outputs.runner_name }} pat_token: ${{ secrets.BOT_TOKEN_GITHUB }} - - update_openapi: - needs: [ deploy ] - runs-on: ubuntu-latest - name: Update OpenAPI - environment: ${{ inputs.environment }} - steps: - - name: Checkout - id: checkout - # from https://github.com/actions/checkout/commits/main - uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 - with: - persist-credentials: false - - - name: Setup Terraform - # from https://github.com/hashicorp/setup-terraform/commits/main - uses: hashicorp/setup-terraform@8feba2b913ea459066180f9cb177f58a881cf146 - with: - terraform_version: "1.3.6" - - - name: Login - id: login - # from https://github.com/Azure/login/commits/master - uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 - with: - client-id: ${{ secrets.CLIENT_ID }} - tenant-id: ${{ secrets.TENANT_ID }} - subscription-id: ${{ secrets.SUBSCRIPTION_ID }} - - - - name: Terraform Apply - shell: bash - run: | - cd ./infra - export ARM_CLIENT_ID="${{ secrets.CLIENT_ID }}" - export ARM_SUBSCRIPTION_ID=$(az account show --query id --output tsv) - export ARM_TENANT_ID=$(az account show --query tenantId --output tsv) - export ARM_USE_OIDC=true - export ARM_ACCESS_KEY=$(az storage account keys list --resource-group io-infra-rg --account-name pagopainfraterraform${{inputs.environment}} --query '[0].value' -o tsv) - bash ./terraform.sh init weu-${{ inputs.environment }} - bash ./terraform.sh apply weu-${{ inputs.environment }} -auto-approve diff --git a/.github/workflows/integration_test.yml b/.github/workflows/05_integration_test.yml similarity index 100% rename from .github/workflows/integration_test.yml rename to .github/workflows/05_integration_test.yml diff --git a/.github/workflows/check_pr.yml b/.github/workflows/check_pr.yml deleted file mode 100644 index cce975c6..00000000 --- a/.github/workflows/check_pr.yml +++ /dev/null @@ -1,183 +0,0 @@ -name: Check PR - -# Controls when the workflow will run -on: - pull_request: - branches: - - main - types: [ opened, synchronize, labeled, unlabeled, reopened, edited ] - - -permissions: - pull-requests: write - - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - auto_assign: - name: Auto Assign - - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - name: Assign Me - # You may pin to the exact commit or the version. - uses: kentaro-m/auto-assign-action@v1.2.1 - with: - configuration-path: '.github/auto_assign.yml' - - check_format: - name: Check Format - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Formatting - id: format - continue-on-error: true - uses: findologic/intellij-format-action@main - with: - path: . - fail-on-changes: false - - - uses: actions/github-script@v6.3.3 - if: steps.format.outcome != 'success' - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - console.log(context); - var comments = await github.rest.issues.listComments({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo - }); - for (const comment of comments.data) { - console.log(comment); - if (comment.body.includes('Comment this PR with')){ - github.rest.issues.deleteComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: comment.id - }) - } - } - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Comment this PR with *update_code* to update `openapi.json` and format the code. Consider to use pre-commit to format the code.' - }) - core.setFailed('Format your code.') - - check_size: - runs-on: ubuntu-latest - name: Check Size - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Check Size - uses: actions/github-script@v6.3.3 - env: - IGNORED_FILES: openapi.json, openapi-node.json - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const additions = context.payload.pull_request.additions || 0 - const deletions = context.payload.pull_request.deletions || 0 - var changes = additions + deletions - console.log('additions: '+additions+' + deletions: '+deletions+ ' = total changes: ' + changes); - - const { IGNORED_FILES } = process.env - const ignored_files = IGNORED_FILES.trim().split(',').filter(word => word.length > 0); - if (ignored_files.length > 0){ - var ignored = 0 - const execSync = require('child_process').execSync; - for (const file of IGNORED_FILES.trim().split(',')) { - - const ignored_additions_str = execSync('git --no-pager diff --numstat origin/main..origin/${{ github.head_ref}} | grep ' + file + ' | cut -f 1', { encoding: 'utf-8' }) - const ignored_deletions_str = execSync('git --no-pager diff --numstat origin/main..origin/${{ github.head_ref}} | grep ' + file + ' | cut -f 2', { encoding: 'utf-8' }) - - const ignored_additions = ignored_additions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( - (accumulator, currentValue) => accumulator + currentValue, - 0); - const ignored_deletions = ignored_deletions_str.split('\n').map(elem=> parseInt(elem || 0)).reduce( - (accumulator, currentValue) => accumulator + currentValue, - 0); - - ignored += ignored_additions + ignored_deletions; - } - changes -= ignored - console.log('ignored lines: ' + ignored + ' , consider changes: ' + changes); - } - - var labels = await github.rest.issues.listLabelsOnIssue({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo - }); - - if (labels.data.find(label => label.name == 'size/large')){ - github.rest.issues.removeLabel({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - name: 'size/large' - }) - } - if (labels.data.find(label => label.name == 'size/small')){ - github.rest.issues.removeLabel({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - name: 'size/small' - }) - } - - var comments = await github.rest.issues.listComments({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo - }); - for (const comment of comments.data) { - if (comment.body.includes('This PR exceeds the recommended size')){ - github.rest.issues.deleteComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: comment.id - }) - } - } - - if (changes < 200){ - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['size/small'] - }) - } - - if (changes > 400){ - github.rest.issues.addLabels({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - labels: ['size/large'] - }) - - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'This PR exceeds the recommended size of 400 lines. Please make sure you are NOT addressing multiple issues with one PR. _Note this PR might be rejected due to its size._' - }) - - } - diff --git a/.github/workflows/code_review.yml b/.github/workflows/code_review.yml deleted file mode 100644 index 3ea47305..00000000 --- a/.github/workflows/code_review.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Code Review - -# Controls when the workflow will run -on: - pull_request: - branches: - - main - types: - - opened - - synchronize - - reopened - push: - branches: - - main - - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -env: - PROJECT_KEY: # TODO - -permissions: - id-token: write - contents: read - deployments: write - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - code-review: - name: Code Review - # The type of runner that the job will run on - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - name: Code Review - uses: pagopa/github-actions-template/maven-code-review@v1.4.2 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - sonar_token: ${{ secrets.SONAR_TOKEN }} - project_key: ${{env.PROJECT_KEY}} - coverage_exclusions: "**/config/*,**/*Mock*,**/model/**,**/entity/*" - cpd_exclusions: "**/model/**,**/entity/*" - - smoke-test: - name: Smoke Test - runs-on: ubuntu-latest - environment: - name: dev - steps: - - name: Checkout - id: checkout - uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 - - - name: Login - id: login - # from https://github.com/Azure/login/commits/master - uses: azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 - with: - client-id: ${{ secrets.CLIENT_ID }} - tenant-id: ${{ secrets.TENANT_ID }} - subscription-id: ${{ secrets.SUBSCRIPTION_ID }} - - - name: Run Service on Docker - shell: bash - id: run_service_docker - run: | - cd ./docker - chmod +x ./run_docker.sh - ./run_docker.sh local - - - name: Run Integration Tests - shell: bash - id: run_integration_test - run: | - export SUBKEY=${{ secrets.SUBKEY }} - export CANARY=${{ inputs.canary }} - export CUCUMBER_PUBLISH_TOKEN=${{ secrets.CUCUMBER_PUBLISH_TOKEN }} - - cd ./integration-test - chmod +x ./run_integration_test.sh - ./run_integration_test.sh local - - - delete_github_deployments: - runs-on: ubuntu-latest - needs: smoke-test - if: ${{ always() }} - steps: - - name: Delete Previous deployments - uses: actions/github-script@v6 - env: - SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} - with: - script: | - const { SHA_HEAD } = process.env - - const deployments = await github.rest.repos.listDeployments({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: SHA_HEAD - }); - await Promise.all( - deployments.data.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); diff --git a/.github/workflows/create_dashboard.yaml b/.github/workflows/create_dashboard.yaml deleted file mode 100644 index 61fa2510..00000000 --- a/.github/workflows/create_dashboard.yaml +++ /dev/null @@ -1,84 +0,0 @@ -name: Create Dashboard - -# Controls when the workflow will run -on: - push: - branches: - - main - paths: - - 'openapi/**' - - '.github/workflows/create_dashboard.yaml' - - '.opex/**' - - workflow_dispatch: - -permissions: - id-token: write - contents: read - deployments: write - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - dashboard: - # The type of runner that the job will run on - runs-on: ubuntu-22.04 - - strategy: - matrix: - environment: [prod] - environment: - name: ${{ matrix.environment }} - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - - name: Checkout - id: checkout - # from https://github.com/actions/checkout/commits/main - uses: actions/checkout@1f9a0c22da41e6ebfa534300ef656657ea2c6707 - with: - persist-credentials: false - - # from https://github.com/pagopa/opex-dashboard-azure-action/ - - uses: pagopa/opex-dashboard-azure-action@v1.1.2 - with: - environment: ${{ matrix.environment }} - api-name: - config: .opex/env/${{ matrix.environment }}/config.yaml - client-id: ${{ secrets.CLIENT_ID }} - tenant-id: ${{ secrets.TENANT_ID }} - subscription-id: ${{ secrets.SUBSCRIPTION_ID }} - # from https://github.com/pagopa/opex-dashboard-azure-action/pkgs/container/opex-dashboard-azure-action - docker-version: sha256:e4245954566cd3470e1b5527d33bb58ca132ce7493eac01be9e808fd25a11c8d - - delete_github_deployments: - runs-on: ubuntu-latest - needs: dashboard - if: ${{ always() }} - steps: - - name: Delete Previous deployments - uses: actions/github-script@v6 - env: - SHA_HEAD: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.sha) || github.sha}} - with: - script: | - const { SHA_HEAD } = process.env - - const deployments = await github.rest.repos.listDeployments({ - owner: context.repo.owner, - repo: context.repo.repo, - sha: SHA_HEAD - }); - await Promise.all( - deployments.data.map(async (deployment) => { - await github.rest.repos.createDeploymentStatus({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id, - state: 'inactive' - }); - return github.rest.repos.deleteDeployment({ - owner: context.repo.owner, - repo: context.repo.repo, - deployment_id: deployment.id - }); - }) - ); diff --git a/.github/workflows/update_code.yml b/.github/workflows/update_code.yml deleted file mode 100644 index ed41d83e..00000000 --- a/.github/workflows/update_code.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Update Code - -on: - issue_comment: - types: [created, edited] - - -permissions: - contents: write - pull-requests: write - issues: write - -jobs: - update: - name: Update Openapi and Formatting - runs-on: ubuntu-latest - if: ${{ contains(github.event.comment.body, 'update_code') }} - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - token: ${{ secrets.BOT_TOKEN_GITHUB }} - - - name: Checkout Pull Request - run: hub pr checkout ${{ github.event.issue.number }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - - name: Update OpenApi/Swagger file - run: | - cd ./openapi - chmod +x ./generate_openapi.sh - ./generate_openapi.sh - - - name: Formatting - id: format - uses: findologic/intellij-format-action@main - with: - path: . - fail-on-changes: false - - - name: Commit files - run: | - git config --local user.email "pagopa-github-bot@pagopa.it" - git config --local user.name "pagopa-github-bot" - git commit -a -m "Formatting" - git push - - - notify: - needs: [ update ] - runs-on: ubuntu-latest - name: Notify - if: ${{ always() && contains(needs.*.result, 'failure') }} - steps: - - name: Notify if Failure - uses: actions/github-script@v6.3.3 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - var comments = await github.rest.issues.listComments({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo - }); - for (const comment of comments.data) { - if (comment.body.includes('Update Code is failed. Please retry.')){ - github.rest.issues.deleteComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: comment.id - }) - } - } - github.rest.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: 'Update Code is failed. Please retry.' - }) - core.setFailed('Update Code is failed. Please retry.') diff --git a/.github/workflows/update_gha.py b/.github/workflows/update_gha.py deleted file mode 100644 index ccb85372..00000000 --- a/.github/workflows/update_gha.py +++ /dev/null @@ -1,19 +0,0 @@ -import requests -import json - - -url = 'https://github.com/pagopa/template-java-spring-microservice/tree/main/.github/workflows' -url_raw = 'https://raw.githubusercontent.com/pagopa/template-java-spring-microservice/main/' - -response = requests.get(url) - -for item in json.loads(response.text)["payload"]["tree"]["items"]: - path = item["path"] - name = item["name"] - print(name) - response = requests.get(url_raw+path) - fo = open(name, "w") - fo.write(response.text) - fo.close() - - diff --git a/.identity/.terraform.lock.hcl b/.identity/.terraform.lock.hcl new file mode 100644 index 00000000..e659e8d8 --- /dev/null +++ b/.identity/.terraform.lock.hcl @@ -0,0 +1,64 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azuread" { + version = "2.30.0" + constraints = "2.30.0" + hashes = [ + "h1:Uw4TcmJBEJ71h+oCwwidlkk5jFpyFRDPAFCMs/bT/cw=", + "zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7", + "zh:2e62c193030e04ebb10cc0526119cf69824bf2d7e4ea5a2f45bd5d5fb7221d36", + "zh:2f3c7a35257332d68b778cefc5201a5f044e4914dd03794a4da662ddfe756483", + "zh:35d0d3a1b58fdb8b8c4462d6b7e7016042da43ea9cc734ce897f52a73407d9b0", + "zh:47ede0cd0206ec953d40bf4a80aa6e59af64e26cbbd877614ac424533dbb693b", + "zh:48c190307d4d42ea67c9b8cc544025024753f46cef6ea64db84735e7055a72da", + "zh:6fff9b2c6a962252a70a15b400147789ab369b35a781e9d21cce3804b04d29af", + "zh:7646980cf3438bff29c91ffedb74458febbb00a996638751fbd204ab1c628c9b", + "zh:77aa2fa7ca6d5446afa71d4ff83cb87b70a2f3b72110fc442c339e8e710b2928", + "zh:e20b2b2c37175b89dd0db058a096544d448032e28e3b56e2db368343533a9684", + "zh:eab175b1dfe9865ad9404dccb6d5542899f8c435095aa7c679314b811c717ce7", + "zh:efc862bd78c55d2ff089729e2a34c1831ab4b0644fc11b36ee4ebed00a4797ba", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.45.0" + constraints = "3.45.0" + hashes = [ + "h1:VQWxV5+qelZeUCjpdLvZ7iAom4RvG+fVVgK6ELvw/cs=", + "zh:04c5dbb8845366ce5eb0dc2d55e151270cc2c0ace20993867fdae9af43b953ad", + "zh:2589585da615ccae341400d45d672ee3fae413fdd88449b5befeff12a85a44b2", + "zh:603869ed98fff5d9bf841a51afd9e06b628533c59356c8433aef4b15df63f5f7", + "zh:853fecab9c987b6772c8d9aa10362675f6c626b60ebc7118aa33ce91366fcc38", + "zh:979848c45e8e058862c36ba3a661457f7c81ef26ebb6634f479600de9c203d65", + "zh:9b512c8588ecc9c1b803b746a3a8517422561a918f0dfb0faaa707ed53ef1760", + "zh:a9601ffb58043426bcff1220662d6d137f0b2857a24f2dcf180aeac2c9cea688", + "zh:d52d2652328f0ed3ba202561d88cb9f43c174edbfaab1abf69f772125dbfe15e", + "zh:d92d91ca597c47f575bf3ae129f4b723be9b7dcb71b906ec6ec740fac29b1aaa", + "zh:ded73b730e4197b70fda9e83447c119f92f75dc37be3ff2ed45730c8f0348c28", + "zh:ec37ac332d50f8ca5827f97198346b0f8ecbf470e2e3ba1e027bb389d826b902", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/integrations/github" { + version = "5.18.3" + constraints = "5.18.3" + hashes = [ + "h1:WbZvLB2qXKVoh4BvOOwFfEds+SZQrkINfSAWPnWFxGo=", + "zh:050b37d96628cb7451137755929ca8d21ea546bc46d11a715652584070e83ff2", + "zh:053051061f1b7f7673b0ceffac1f239ba28b0e5b375999206fd39976e85d9f2b", + "zh:0c300a977ca66d0347ed62bb116fd8fc9abb376a554d4c192d14f3ea71c83500", + "zh:1d5a1a5243eba78819d2f92ff2d504ebf9a9008a6670fb5f5660f44eb6a156d8", + "zh:a13ac15d251ebf4e7dc40acb0e40df066f443f4c7799186a29e2e44addc7d8e7", + "zh:a316d94b885953c036ebc9fba64a23da93974746bc3ac9d207462a6f02d44540", + "zh:a658a00373bff5979cc227052c693cbde8ca4c8f9fef1bc8094a3516f2e2a96d", + "zh:a7bfc6ad8465d5dc11b6f19d6805364de87fffe27622bb4f37da2319bb1c4956", + "zh:d7379a76861f1a6bfc36eca7a20f1f477711247563b105744d69d7bd1f365fad", + "zh:de1cd959fd4821248e8d21570601193408648474e74f49597f1d0c43185a4ab7", + "zh:e0b281240dd6f2aa405b2d6fe329bc15ab877161affe163fb150d1efca2fccdb", + "zh:e372c171358757a983d7aa878abfd05a84484fb4d22167e45c9c1267e78ed060", + "zh:f6d3116526030b3f6905f530cd6c04b23d42890d973fa2abe10ce9c89cb1db80", + "zh:f99eec731e03cc6a28996c875bd435887cd7ea75ec07cc77b9e768bb12da2227", + ] +} diff --git a/.identity/00_data.tf b/.identity/00_data.tf index 8273e727..f1433eb2 100644 --- a/.identity/00_data.tf +++ b/.identity/00_data.tf @@ -1,4 +1,4 @@ -data "azurerm_storage_account" "tf_storage_account"{ +data "azurerm_storage_account" "tf_storage_account" { name = "pagopainfraterraform${var.env}" resource_group_name = "io-infra-rg" } @@ -7,6 +7,10 @@ data "azurerm_resource_group" "dashboards" { name = "dashboards" } +data "azurerm_resource_group" "apim_resource_group" { + name = "${local.product}-api-rg" +} + data "azurerm_kubernetes_cluster" "aks" { name = local.aks_cluster.name resource_group_name = local.aks_cluster.resource_group_name @@ -27,10 +31,6 @@ data "azurerm_key_vault" "domain_key_vault" { resource_group_name = "pagopa-${var.env_short}-${local.domain}-sec-rg" } -data "azurerm_resource_group" "apim_resource_group" { - name = "${local.product}-api-rg" -} - data "azurerm_key_vault_secret" "key_vault_sonar" { name = "sonar-token" key_vault_id = data.azurerm_key_vault.key_vault.id @@ -50,3 +50,8 @@ data "azurerm_key_vault_secret" "key_vault_integration_test_subkey" { name = "integration-test-subkey" key_vault_id = data.azurerm_key_vault.key_vault.id } + +data "azurerm_user_assigned_identity" "identity_cd" { + name = "${local.product}-${local.domain}-01-github-cd-identity" + resource_group_name = "${local.product}-identity-rg" +} \ No newline at end of file diff --git a/.identity/02_application_action.tf b/.identity/02_application_action.tf deleted file mode 100644 index d6a7a245..00000000 --- a/.identity/02_application_action.tf +++ /dev/null @@ -1,96 +0,0 @@ -module "github_runner_app" { - source = "git::https://github.com/pagopa/github-actions-tf-modules.git//app-github-runner-creator?ref=main" - - app_name = local.app_name - - subscription_id = data.azurerm_subscription.current.id - - github_org = local.github.org - github_repository = local.github.repository - github_environment_name = var.env - - container_app_github_runner_env_rg = local.container_app_environment.resource_group -} - -resource "null_resource" "github_runner_app_permissions_to_namespace" { - triggers = { - aks_id = data.azurerm_kubernetes_cluster.aks.id - service_principal_id = module.github_runner_app.client_id - namespace = local.domain - version = "v2" - } - - provisioner "local-exec" { - command = < - 4.0.0 + xmlns="http://maven.apache.org/POM/4.0.0" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.7.3 - - - it.gov.pagopa - microservice - 0.0.0 - Your Name - Your description - - - 11 - - - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-validation - - - - - org.springframework.boot - spring-boot-devtools - runtime - true - - - org.springframework.boot - spring-boot-configuration-processor - true - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-starter-actuator - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.springframework.data - spring-data-jpa - - - - org.springframework.boot - spring-boot-starter-cache - - - com.github.ben-manes.caffeine - caffeine - - - - - org.springdoc - springdoc-openapi-ui - 1.6.11 - - - - com.h2database - h2 - runtime - - - - org.hibernate.orm - hibernate-core - 6.1.3.Final - - - - org.springframework.cloud - spring-cloud-starter-openfeign - 4.0.3 - - - - - org.modelmapper - modelmapper - 3.1.0 - - - org.projectlombok - lombok - true - - - - junit - junit - test - - - co.elastic.logging - logback-ecs-encoder - 1.5.0 - - - - - - + org.springframework.boot - spring-boot-maven-plugin - - - - org.jacoco - jacoco-maven-plugin - 0.8.7 - - - - - - - - prepare-agent - - - - report - test - - report - - - - - - org.sonarsource.scanner.maven - sonar-maven-plugin - 3.3.0.603 - - - verify - - sonar - - - - - - - - src/test/resources - true - - - + spring-boot-starter-parent + 2.7.3 + + + it.gov.pagopa + wisp-converter + 0.0.0 + pagoPA WISP Converter + A service that permits to handle nodoInviaRPT and nodoInviaCarrelloRPT request from WISP, interfacing + them with GPD system + + + + 17 + 17 + UTF-8 + 1.6.15 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web-services + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.data + spring-data-jpa + + + org.springframework.boot + spring-boot-starter-cache + + + com.github.ben-manes.caffeine + caffeine + + + + + org.springdoc + springdoc-openapi-ui + ${springdoc.version} + + + + org.hibernate.orm + hibernate-core + 6.1.3.Final + + + + org.springframework.cloud + spring-cloud-starter-openfeign + 4.0.3 + + + + + org.modelmapper + modelmapper + 3.1.0 + + + org.projectlombok + lombok + true + + + + junit + junit + test + + + co.elastic.logging + logback-ecs-encoder + 1.5.0 + + + + + + + local + + + mac + aarch64 + + + + + io.netty + netty-resolver-dns-native-macos + osx-aarch_64 + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.jacoco + jacoco-maven-plugin + 0.8.7 + + + + + + + + prepare-agent + + + + report + test + + report + + + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.3.0.603 + + + verify + + sonar + + + + + + + + src/test/resources + true + + + diff --git a/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java b/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java deleted file mode 100644 index 6b7ec184..00000000 --- a/src/main/java/it/gov/pagopa/microservice/config/OpenApiConfig.java +++ /dev/null @@ -1,129 +0,0 @@ -package it.gov.pagopa.microservice.config; - -import static it.gov.pagopa.microservice.util.Constants.HEADER_REQUEST_ID; - -import io.swagger.v3.oas.models.Components; -import io.swagger.v3.oas.models.OpenAPI; -import io.swagger.v3.oas.models.Paths; -import io.swagger.v3.oas.models.headers.Header; -import io.swagger.v3.oas.models.info.Info; -import io.swagger.v3.oas.models.media.StringSchema; -import io.swagger.v3.oas.models.parameters.Parameter; -import io.swagger.v3.oas.models.responses.ApiResponses; -import io.swagger.v3.oas.models.security.SecurityScheme; -import java.util.Collections; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import org.springdoc.core.customizers.OpenApiCustomiser; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class OpenApiConfig { - - @Bean - public OpenAPI customOpenAPI( - @Value("${info.application.artifactId}") String appName, - @Value("${info.application.description}") String appDescription, - @Value("${info.application.version}") String appVersion) { - return new OpenAPI() - .components( - new Components() - .addSecuritySchemes( - "ApiKey", - new SecurityScheme() - .type(SecurityScheme.Type.APIKEY) - .description("The API key to access this function app.") - .name("Ocp-Apim-Subscription-Key") - .in(SecurityScheme.In.HEADER))) - .info( - new Info() - .title(appName) - .version(appVersion) - .description(appDescription) - .termsOfService("https://www.pagopa.gov.it/")); - } - - @Bean - public OpenApiCustomiser sortOperationsAlphabetically() { - return openApi -> { - Paths paths = - openApi - .getPaths() - .entrySet() - .stream() - .sorted(Map.Entry.comparingByKey()) - .collect( - Paths::new, - (map, item) -> map.addPathItem(item.getKey(), item.getValue()), - Paths::putAll); - - paths.forEach( - (key, value) -> - value - .readOperations() - .forEach( - operation -> { - var responses = - operation - .getResponses() - .entrySet() - .stream() - .sorted(Map.Entry.comparingByKey()) - .collect( - ApiResponses::new, - (map, item) -> - map.addApiResponse(item.getKey(), item.getValue()), - ApiResponses::putAll); - operation.setResponses(responses); - })); - openApi.setPaths(paths); - }; - } - - @Bean - public OpenApiCustomiser addCommonHeaders() { - return openApi -> - openApi - .getPaths() - .forEach( - (key, value) -> { - - // add Request-ID as request header - var header = - Optional.ofNullable(value.getParameters()) - .orElse(Collections.emptyList()) - .parallelStream() - .filter(Objects::nonNull) - .anyMatch(elem -> HEADER_REQUEST_ID.equals(elem.getName())); - if (!header) { - value.addParametersItem( - new Parameter() - .in("header") - .name(HEADER_REQUEST_ID) - .schema(new StringSchema()) - .description( - "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.")); - } - - // add Request-ID as response header - value - .readOperations() - .forEach( - operation -> - operation - .getResponses() - .values() - .forEach( - response -> - response.addHeaderObject( - HEADER_REQUEST_ID, - new Header() - .schema(new StringSchema()) - .description( - "This header identifies the call")))); - }); - } -} diff --git a/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java b/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java deleted file mode 100644 index 63726e30..00000000 --- a/src/main/java/it/gov/pagopa/microservice/config/RequestFilter.java +++ /dev/null @@ -1,58 +0,0 @@ -package it.gov.pagopa.microservice.config; - -import static it.gov.pagopa.microservice.util.Constants.HEADER_REQUEST_ID; - -import java.io.IOException; -import java.util.UUID; -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import lombok.extern.slf4j.Slf4j; -import org.slf4j.MDC; -import org.springframework.core.Ordered; -import org.springframework.core.annotation.Order; -import org.springframework.stereotype.Component; - -@Component -@Order(Ordered.HIGHEST_PRECEDENCE) -@Slf4j -public class RequestFilter implements Filter { - - /** - * Get the request ID from the custom header "X-Request-Id" if present, otherwise it generates - * one. Set the X-Request-Id value in the {@code response} and in the MDC - * - * @param request http request - * @param response http response - * @param chain next filter - * @throws IOException if an I/O error occurs during this filter's processing of the request - * @throws ServletException if the processing fails for any other reason - */ - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - try { - HttpServletRequest httRequest = (HttpServletRequest) request; - - // get requestId from header or generate one - String requestId = httRequest.getHeader(HEADER_REQUEST_ID); - if (requestId == null || requestId.isEmpty()) { - requestId = UUID.randomUUID().toString(); - } - - // set requestId in MDC - MDC.put("requestId", requestId); - - // set requestId in the response header - ((HttpServletResponse) response).setHeader(HEADER_REQUEST_ID, requestId); - chain.doFilter(request, response); - } finally { - MDC.clear(); - } - } - -} diff --git a/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java b/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java deleted file mode 100644 index 67cc3af3..00000000 --- a/src/main/java/it/gov/pagopa/microservice/config/ResponseValidator.java +++ /dev/null @@ -1,52 +0,0 @@ -package it.gov.pagopa.microservice.config; - -import it.gov.pagopa.microservice.exception.AppException; -import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import org.apache.commons.lang3.StringUtils; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.AfterReturning; -import org.aspectj.lang.annotation.Aspect; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Component; - -@Aspect -@Component -public class ResponseValidator { - - @Autowired - private Validator validator; - - - /** - * This method validates the response annotated with the {@link javax.validation.constraints} - * - * @param joinPoint not used - * @param result the response to validate - */ - // TODO: set your package - @AfterReturning(pointcut = "execution(* it.gov.pagopa.microservice.controller.*.*(..))", returning = "result") - public void validateResponse(JoinPoint joinPoint, Object result) { - if (result instanceof ResponseEntity) { - validateResponse((ResponseEntity) result); - } - } - - private void validateResponse(ResponseEntity response) { - if (response.getBody() != null) { - Set> validationResults = validator.validate(response.getBody()); - - if (!validationResults.isEmpty()) { - var sb = new StringBuilder(); - for (ConstraintViolation error : validationResults) { - sb.append(error.getPropertyPath()).append(" ").append(error.getMessage()).append(". "); - } - var msg = StringUtils.chop(sb.toString()); - throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid response", msg); - } - } - } -} diff --git a/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java b/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java deleted file mode 100644 index c61db336..00000000 --- a/src/main/java/it/gov/pagopa/microservice/config/WebMvcConfiguration.java +++ /dev/null @@ -1,30 +0,0 @@ -package it.gov.pagopa.microservice.config; - -import com.fasterxml.jackson.databind.ObjectMapper; -import it.gov.pagopa.microservice.model.AppCorsConfiguration; -import lombok.SneakyThrows; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - - -@Configuration -public class WebMvcConfiguration implements WebMvcConfigurer { - - @Value("${cors.configuration}") - private String corsConfiguration; - - - @SneakyThrows - @Override - public void addCorsMappings(CorsRegistry registry) { - AppCorsConfiguration appCorsConfiguration = new ObjectMapper().readValue(corsConfiguration, - AppCorsConfiguration.class); - registry.addMapping("/**") - .allowedOrigins(appCorsConfiguration.getOrigins()) - .allowedMethods(appCorsConfiguration.getMethods()); - } -} - - diff --git a/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java b/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java deleted file mode 100644 index 70b6dd11..00000000 --- a/src/main/java/it/gov/pagopa/microservice/controller/HomeController.java +++ /dev/null @@ -1,30 +0,0 @@ -package it.gov.pagopa.microservice.controller; - -import io.swagger.v3.oas.annotations.Hidden; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.view.RedirectView; - -@RestController -@Validated -public class HomeController { - - @Value("${server.servlet.context-path}") - String basePath; - - - /** - * @return redirect to Swagger page documentation - */ - @Hidden - @GetMapping("") - public RedirectView home() { - if (!basePath.endsWith("/")) { - basePath += "/"; - } - return new RedirectView(basePath + "swagger-ui.html"); - } - -} diff --git a/src/main/java/it/gov/pagopa/microservice/exception/AppError.java b/src/main/java/it/gov/pagopa/microservice/exception/AppError.java deleted file mode 100644 index d9ccb08e..00000000 --- a/src/main/java/it/gov/pagopa/microservice/exception/AppError.java +++ /dev/null @@ -1,30 +0,0 @@ -package it.gov.pagopa.microservice.exception; - -import lombok.Getter; -import org.springframework.http.HttpStatus; - - -@Getter -public enum AppError { - INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "Something was wrong"), - BAD_REQUEST(HttpStatus.INTERNAL_SERVER_ERROR, "Bad Request", "%s"), - UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized", "Error during authentication"), - FORBIDDEN(HttpStatus.FORBIDDEN, "Forbidden", "This method is forbidden"), - RESPONSE_NOT_READABLE(HttpStatus.BAD_GATEWAY, "Response Not Readable", "The response body is not readable"), - - UNKNOWN(null, null, null); - - - public final HttpStatus httpStatus; - public final String title; - public final String details; - - - AppError(HttpStatus httpStatus, String title, String details) { - this.httpStatus = httpStatus; - this.title = title; - this.details = details; - } -} - - diff --git a/src/main/java/it/gov/pagopa/microservice/exception/AppException.java b/src/main/java/it/gov/pagopa/microservice/exception/AppException.java deleted file mode 100644 index e16fd36a..00000000 --- a/src/main/java/it/gov/pagopa/microservice/exception/AppException.java +++ /dev/null @@ -1,87 +0,0 @@ -package it.gov.pagopa.microservice.exception; - -import java.util.Formatter; -import javax.validation.constraints.NotNull; -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.springframework.http.HttpStatus; -import org.springframework.validation.annotation.Validated; - -/** - * Custom exception. - *

See {@link ErrorHandler} - */ -@EqualsAndHashCode(callSuper = true) -@Value -@Validated -public class AppException extends RuntimeException { - - /** - * title returned to the response when this exception occurred - */ - String title; - - /** - * http status returned to the response when this exception occurred - */ - HttpStatus httpStatus; - - /** - * @param httpStatus HTTP status returned to the response - * @param title title returned to the response when this exception occurred - * @param message the detail message returend to the response - * @param cause The cause of this {@link AppException} - */ - public AppException(@NotNull HttpStatus httpStatus, @NotNull String title, - @NotNull String message, Throwable cause) { - super(message, cause); - this.title = title; - this.httpStatus = httpStatus; - } - - /** - * @param httpStatus HTTP status returned to the response - * @param title title returned to the response when this exception occurred - * @param message the detail message returend to the response - */ - public AppException(@NotNull HttpStatus httpStatus, @NotNull String title, @NotNull String message) { - super(message); - this.title = title; - this.httpStatus = httpStatus; - } - - - /** - * @param appError Response template returned to the response - * @param args {@link Formatter} replaces the placeholders in "details" string of - * {@link AppError} with the arguments. If there are more arguments than format - * specifiers, the extra arguments are ignored. - */ - public AppException(@NotNull AppError appError, Object... args) { - super(formatDetails(appError, args)); - this.httpStatus = appError.httpStatus; - this.title = appError.title; - } - - /** - * @param appError Response template returned to the response - * @param cause The cause of this {@link AppException} - * @param args Arguments for the details of {@link AppError} replaced by the - * {@link Formatter}. If there are more arguments than format specifiers, the - * extra arguments are ignored. - */ - public AppException(@NotNull AppError appError, Throwable cause, Object... args) { - super(formatDetails(appError, args), cause); - this.httpStatus = appError.httpStatus; - this.title = appError.title; - } - - private static String formatDetails(AppError appError, Object[] args) { - return String.format(appError.details, args); - } - - @Override - public String toString() { - return "AppException(" + httpStatus + ", " + title + ")" + super.toString(); - } -} diff --git a/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java b/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java deleted file mode 100644 index 3fd7e497..00000000 --- a/src/main/java/it/gov/pagopa/microservice/exception/ErrorHandler.java +++ /dev/null @@ -1,220 +0,0 @@ -package it.gov.pagopa.microservice.exception; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import feign.FeignException; -import it.gov.pagopa.microservice.model.ProblemJson; -import java.util.ArrayList; -import java.util.List; -import lombok.extern.slf4j.Slf4j; -import org.hibernate.exception.ConstraintViolationException; -import org.springframework.beans.TypeMismatchException; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.MissingServletRequestParameterException; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.context.request.WebRequest; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -/** - * All Exceptions are handled by this class - */ -@ControllerAdvice -@Slf4j -public class ErrorHandler extends ResponseEntityExceptionHandler { - - /** - * Handle if the input request is not a valid JSON - * - * @param ex {@link HttpMessageNotReadableException} exception raised - * @param headers of the response - * @param status of the response - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status - */ - @Override - public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, - HttpHeaders headers, HttpStatus status, WebRequest request) { - log.warn("Input not readable: ", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail("Invalid input format") - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - /** - * Handle if missing some request parameters in the request - * - * @param ex {@link MissingServletRequestParameterException} exception raised - * @param headers of the response - * @param status of the response - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status - */ - @Override - public ResponseEntity handleMissingServletRequestParameter( - MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, - WebRequest request) { - log.warn("Missing request parameter: ", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - - /** - * Customize the response for TypeMismatchException. - * - * @param ex the exception - * @param headers the headers to be written to the response - * @param status the selected response status - * @param request the current request - * @return a {@code ResponseEntity} instance - */ - @Override - protected ResponseEntity handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, - HttpStatus status, WebRequest request) { - log.warn("Type mismatch: ", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(String.format("Invalid value %s for property %s", ex.getValue(), - ((MethodArgumentTypeMismatchException) ex).getName())) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - /** - * Handle if validation constraints are unsatisfied - * - * @param ex {@link MethodArgumentNotValidException} exception raised - * @param headers of the response - * @param status of the response - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status - */ - @Override - protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, - HttpHeaders headers, HttpStatus status, WebRequest request) { - List details = new ArrayList<>(); - for (FieldError error : ex.getBindingResult().getFieldErrors()) { - details.add(error.getField() + ": " + error.getDefaultMessage()); - } - var detailsMessage = String.join(", ", details); - log.warn("Input not valid: " + detailsMessage); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(detailsMessage) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - @ExceptionHandler({javax.validation.ConstraintViolationException.class}) - public ResponseEntity handleConstraintViolationException( - final javax.validation.ConstraintViolationException ex, final WebRequest request) { - log.warn("Validation Error raised:", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.BAD_REQUEST.value()) - .title(AppError.BAD_REQUEST.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); - } - - - /** - * Handle if a {@link FeignException} is raised - * - * @param ex {@link FeignException} exception raised - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status - */ - @ExceptionHandler({FeignException.class}) - public ResponseEntity handleFeignException(final FeignException ex, final WebRequest request) { - log.warn("FeignException raised: ", ex); - - ProblemJson problem; - if(ex.responseBody().isPresent()) { - var body = new String(ex.responseBody().get().array(), StandardCharsets.UTF_8); - try { - problem = new ObjectMapper().readValue(body, ProblemJson.class); - } catch (JsonProcessingException e) { - problem = ProblemJson.builder() - .status(HttpStatus.BAD_GATEWAY.value()) - .title(AppError.RESPONSE_NOT_READABLE.getTitle()) - .detail(AppError.RESPONSE_NOT_READABLE.getDetails()) - .build(); - } - } else { - problem = ProblemJson.builder() - .status(HttpStatus.BAD_GATEWAY.value()) - .title("No Response Body") - .detail("Error with external dependency") - .build(); - } - - return new ResponseEntity<>(problem, HttpStatus.valueOf(problem.getStatus())); - } - - - /** - * Handle if a {@link AppException} is raised - * - * @param ex {@link AppException} exception raised - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status - */ - @ExceptionHandler({AppException.class}) - public ResponseEntity handleAppException(final AppException ex, - final WebRequest request) { - if (ex.getCause() != null) { - log.warn("App Exception raised: " + ex.getMessage() + "\nCause of the App Exception: ", - ex.getCause()); - } else { - log.warn("App Exception raised: ", ex); - } - var errorResponse = ProblemJson.builder() - .status(ex.getHttpStatus().value()) - .title(ex.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, ex.getHttpStatus()); - } - - - /** - * Handle if a {@link Exception} is raised - * - * @param ex {@link Exception} exception raised - * @param request from frontend - * @return a {@link ProblemJson} as response with the cause and with 500 as HTTP status - */ - @ExceptionHandler({Exception.class}) - public ResponseEntity handleGenericException(final Exception ex, - final WebRequest request) { - log.error("Generic Exception raised:", ex); - var errorResponse = ProblemJson.builder() - .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) - .title(AppError.INTERNAL_SERVER_ERROR.getTitle()) - .detail(ex.getMessage()) - .build(); - return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); - } -} diff --git a/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java b/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java deleted file mode 100644 index db11a370..00000000 --- a/src/main/java/it/gov/pagopa/microservice/model/ProblemJson.java +++ /dev/null @@ -1,40 +0,0 @@ -package it.gov.pagopa.microservice.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; -import io.swagger.v3.oas.annotations.media.Schema; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import lombok.ToString; - -/** - * Object returned as response in case of an error. - *

See {@link it.pagopa.microservice.exception.ErrorHandler} - */ -@Data -@Builder(toBuilder = true) -@NoArgsConstructor -@AllArgsConstructor -@ToString -@JsonIgnoreProperties(ignoreUnknown = true) -public class ProblemJson { - - @JsonProperty("title") - @Schema(description = "A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable") - private String title; - - @JsonProperty("status") - @Schema(example = "200", description = "The HTTP status code generated by the origin server for this occurrence of the problem.") - @Min(100) - @Max(600) - private Integer status; - - @JsonProperty("detail") - @Schema(example = "There was an error processing the request", description = "A human readable explanation specific to this occurrence of the problem.") - private String detail; - -} diff --git a/src/main/java/it/gov/pagopa/microservice/util/Constants.java b/src/main/java/it/gov/pagopa/microservice/util/Constants.java deleted file mode 100644 index e5812c73..00000000 --- a/src/main/java/it/gov/pagopa/microservice/util/Constants.java +++ /dev/null @@ -1,11 +0,0 @@ -package it.gov.pagopa.microservice.util; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public class Constants { - - - public static final String HEADER_REQUEST_ID = "X-Request-Id"; - -} diff --git a/src/main/java/it/gov/pagopa/microservice/Application.java b/src/main/java/it/gov/pagopa/wispconverter/Application.java similarity index 51% rename from src/main/java/it/gov/pagopa/microservice/Application.java rename to src/main/java/it/gov/pagopa/wispconverter/Application.java index 465b6cb3..35df271d 100644 --- a/src/main/java/it/gov/pagopa/microservice/Application.java +++ b/src/main/java/it/gov/pagopa/wispconverter/Application.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice; // TODO: refactor the package +package it.gov.pagopa.wispconverter; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -6,8 +6,8 @@ @SpringBootApplication public class Application { - public static void main(String[] args) { - SpringApplication.run(Application.class, args); - } + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } } diff --git a/src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java b/src/main/java/it/gov/pagopa/wispconverter/config/LoggingAspect.java similarity index 91% rename from src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java rename to src/main/java/it/gov/pagopa/wispconverter/config/LoggingAspect.java index 89906328..f7768dea 100644 --- a/src/main/java/it/gov/pagopa/microservice/config/LoggingAspect.java +++ b/src/main/java/it/gov/pagopa/wispconverter/config/LoggingAspect.java @@ -1,7 +1,7 @@ -package it.gov.pagopa.microservice.config; +package it.gov.pagopa.wispconverter.config; -import it.gov.pagopa.microservice.exception.AppError; -import it.gov.pagopa.microservice.model.ProblemJson; +import it.gov.pagopa.wispconverter.exception.AppError; +import it.gov.pagopa.wispconverter.model.ProblemJson; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; @@ -23,7 +23,7 @@ import java.util.Map; import java.util.UUID; -import static it.gov.pagopa.microservice.util.CommonUtility.deNull; +import static it.gov.pagopa.wispconverter.util.CommonUtility.deNull; @Aspect @@ -57,6 +57,37 @@ public class LoggingAspect { @Value("${info.properties.environment}") private String environment; + private static String getDetail(ResponseEntity result) { + if (result != null && result.getBody() != null && result.getBody().getDetail() != null) { + return result.getBody().getDetail(); + } else return AppError.UNKNOWN.getDetails(); + } + + private static String getTitle(ResponseEntity result) { + if (result != null && result.getBody() != null && result.getBody().getTitle() != null) { + return result.getBody().getTitle(); + } else return AppError.UNKNOWN.getTitle(); + } + + public static String getExecutionTime() { + String startTime = MDC.get(START_TIME); + if (startTime != null) { + long endTime = System.currentTimeMillis(); + long executionTime = endTime - Long.parseLong(startTime); + return String.valueOf(executionTime); + } + return "-"; + } + + private static Map getParams(ProceedingJoinPoint joinPoint) { + CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); + Map params = new HashMap<>(); + int i = 0; + for (var paramName : codeSignature.getParameterNames()) { + params.put(paramName, deNull(joinPoint.getArgs()[i++])); + } + return params; + } @Pointcut("@within(org.springframework.web.bind.annotation.RestController)") public void restController() { @@ -86,7 +117,7 @@ public Object logApiInvocation(ProceedingJoinPoint joinPoint) throws Throwable { MDC.put(METHOD, joinPoint.getSignature().getName()); MDC.put(START_TIME, String.valueOf(System.currentTimeMillis())); MDC.put(OPERATION_ID, UUID.randomUUID().toString()); - if(MDC.get(REQUEST_ID) == null) { + if (MDC.get(REQUEST_ID) == null) { var requestId = UUID.randomUUID().toString(); MDC.put(REQUEST_ID, requestId); } @@ -127,36 +158,4 @@ public Object logTrace(ProceedingJoinPoint joinPoint) throws Throwable { log.debug("Return method {} - result: {}", joinPoint.getSignature().toShortString(), result); return result; } - - private static String getDetail(ResponseEntity result) { - if(result != null && result.getBody() != null && result.getBody().getDetail() != null) { - return result.getBody().getDetail(); - } else return AppError.UNKNOWN.getDetails(); - } - - private static String getTitle(ResponseEntity result) { - if(result != null && result.getBody() != null && result.getBody().getTitle() != null) { - return result.getBody().getTitle(); - } else return AppError.UNKNOWN.getTitle(); - } - - public static String getExecutionTime() { - String startTime = MDC.get(START_TIME); - if(startTime != null) { - long endTime = System.currentTimeMillis(); - long executionTime = endTime - Long.parseLong(startTime); - return String.valueOf(executionTime); - } - return "-"; - } - - private static Map getParams(ProceedingJoinPoint joinPoint) { - CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature(); - Map params = new HashMap<>(); - int i = 0; - for (var paramName : codeSignature.getParameterNames()) { - params.put(paramName, deNull(joinPoint.getArgs()[i++])); - } - return params; - } } diff --git a/src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java b/src/main/java/it/gov/pagopa/wispconverter/config/MappingsConfiguration.java similarity index 51% rename from src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java rename to src/main/java/it/gov/pagopa/wispconverter/config/MappingsConfiguration.java index b403822e..5c94b060 100644 --- a/src/main/java/it/gov/pagopa/microservice/config/MappingsConfiguration.java +++ b/src/main/java/it/gov/pagopa/wispconverter/config/MappingsConfiguration.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.config; +package it.gov.pagopa.wispconverter.config; import org.modelmapper.ModelMapper; @@ -9,11 +9,11 @@ @Configuration public class MappingsConfiguration { - @Bean - ModelMapper modelMapper() { - ModelMapper mapper = new ModelMapper(); - mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); - return mapper; - } + @Bean + ModelMapper modelMapper() { + ModelMapper mapper = new ModelMapper(); + mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT); + return mapper; + } } diff --git a/src/main/java/it/gov/pagopa/wispconverter/config/OpenAPITableMetadataCustomizer.java b/src/main/java/it/gov/pagopa/wispconverter/config/OpenAPITableMetadataCustomizer.java new file mode 100644 index 00000000..f670a675 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/config/OpenAPITableMetadataCustomizer.java @@ -0,0 +1,48 @@ +package it.gov.pagopa.wispconverter.config; + +import io.swagger.v3.oas.models.Operation; +import it.gov.pagopa.wispconverter.util.OpenAPITableMetadata; +import org.springdoc.core.customizers.OperationCustomizer; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; + +import static it.gov.pagopa.wispconverter.util.CommonUtility.deNull; + +@Component +public class OpenAPITableMetadataCustomizer implements OperationCustomizer { + + private static final String SEPARATOR = " | "; + + private static String buildData(OpenAPITableMetadata annotation) { + return "**API properties:**\n" + + "Property" + SEPARATOR + "Value\n" + + "-" + SEPARATOR + "-\n" + + "Internal" + SEPARATOR + parseBoolToYN(annotation.internal()) + "\n" + + "External" + SEPARATOR + parseBoolToYN(annotation.external()) + "\n" + + "Synchronous" + SEPARATOR + annotation.synchronism() + "\n" + + "Authorization" + SEPARATOR + annotation.authorization() + "\n" + + "Authentication" + SEPARATOR + annotation.authentication() + "\n" + + "TPS" + SEPARATOR + annotation.tps() + "/sec" + "\n" + + "Idempotency" + SEPARATOR + parseBoolToYN(annotation.idempotency()) + "\n" + + "Stateless" + SEPARATOR + parseBoolToYN(annotation.stateless()) + "\n" + + "Read/Write Intense" + SEPARATOR + parseReadWrite(annotation.readWriteIntense()) + "\n" + + "Cacheable" + SEPARATOR + parseBoolToYN(annotation.cacheable()) + "\n"; + } + + private static String parseReadWrite(OpenAPITableMetadata.ReadWrite readWrite) { + return readWrite.getValue(); + } + + private static String parseBoolToYN(boolean value) { + return value ? "Y" : "N"; + } + + @Override + public Operation customize(Operation operation, HandlerMethod handlerMethod) { + OpenAPITableMetadata annotation = handlerMethod.getMethodAnnotation(OpenAPITableMetadata.class); + if (annotation != null) { + operation.description("**Description:** \n" + deNull(operation.getDescription()) + " \n\n" + buildData(annotation)); + } + return operation; + } +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/wispconverter/config/OpenApiConfig.java b/src/main/java/it/gov/pagopa/wispconverter/config/OpenApiConfig.java new file mode 100644 index 00000000..b6ad0091 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/config/OpenApiConfig.java @@ -0,0 +1,131 @@ +package it.gov.pagopa.wispconverter.config; + +import static it.gov.pagopa.wispconverter.util.Constants.HEADER_REQUEST_ID; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.oas.models.headers.Header; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.security.SecurityScheme; + +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import org.springdoc.core.customizers.OpenApiCustomiser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenApiConfig { + + @Bean + public OpenAPI customOpenAPI( + @Value("${info.application.artifactId}") String appName, + @Value("${info.application.description}") String appDescription, + @Value("${info.application.version}") String appVersion) { + return new OpenAPI() + .components( + new Components() + .addSecuritySchemes( + "ApiKey", + new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) + .description("The API key to access this function app.") + .name("Ocp-Apim-Subscription-Key") + .in(SecurityScheme.In.HEADER))) + .info( + new Info() + .title(appName) + .version(appVersion) + .description(appDescription) + .termsOfService("https://www.pagopa.gov.it/")); + } + + @Bean + public OpenApiCustomiser sortOperationsAlphabetically() { + return openApi -> { + Paths paths = + openApi + .getPaths() + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .collect( + Paths::new, + (map, item) -> map.addPathItem(item.getKey(), item.getValue()), + Paths::putAll); + + paths.forEach( + (key, value) -> + value + .readOperations() + .forEach( + operation -> { + var responses = + operation + .getResponses() + .entrySet() + .stream() + .sorted(Map.Entry.comparingByKey()) + .collect( + ApiResponses::new, + (map, item) -> + map.addApiResponse(item.getKey(), item.getValue()), + ApiResponses::putAll); + operation.setResponses(responses); + })); + openApi.setPaths(paths); + }; + } + + @Bean + public OpenApiCustomiser addCommonHeaders() { + return openApi -> + openApi + .getPaths() + .forEach( + (key, value) -> { + + // add Request-ID as request header + var header = + Optional.ofNullable(value.getParameters()) + .orElse(Collections.emptyList()) + .parallelStream() + .filter(Objects::nonNull) + .anyMatch(elem -> HEADER_REQUEST_ID.equals(elem.getName())); + if (!header) { + value.addParametersItem( + new Parameter() + .in("header") + .name(HEADER_REQUEST_ID) + .schema(new StringSchema()) + .description( + "This header identifies the call, if not passed it is self-generated. This ID is returned in the response.")); + } + + // add Request-ID as response header + value + .readOperations() + .forEach( + operation -> + operation + .getResponses() + .values() + .forEach( + response -> + response.addHeaderObject( + HEADER_REQUEST_ID, + new Header() + .schema(new StringSchema()) + .description( + "This header identifies the call")))); + }); + } +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/config/RedisConfig.java b/src/main/java/it/gov/pagopa/wispconverter/config/RedisConfig.java new file mode 100644 index 00000000..49a83ad4 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/config/RedisConfig.java @@ -0,0 +1,55 @@ +package it.gov.pagopa.wispconverter.config; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Value("${spring.redis.host}") + private String host; + + @Value("${spring.redis.port}") + private int port; + + @Value("${spring.redis.password}") + private String password; + + @Bean + public ObjectMapper registerObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + return objectMapper; + } + + @Bean + public LettuceConnectionFactory registerRedisConnectionFactory() { + RedisStandaloneConfiguration redisConfiguration = new RedisStandaloneConfiguration(host, port); + redisConfiguration.setPassword(password); + LettuceClientConfiguration lettuceConfig = LettuceClientConfiguration.builder().useSsl().build(); + return new LettuceConnectionFactory(redisConfiguration, lettuceConfig); + } + + @Bean + @Qualifier("object") + public RedisTemplate registerRedisSimpleTemplate(final LettuceConnectionFactory connectionFactory, ObjectMapper objectMapper) { + RedisTemplate template = new RedisTemplate<>(); + template.setKeySerializer(new StringRedisSerializer()); + Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); + redisSerializer.setObjectMapper(objectMapper); + template.setValueSerializer(redisSerializer); + template.setConnectionFactory(connectionFactory); + return template; + } +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/wispconverter/config/RequestFilter.java b/src/main/java/it/gov/pagopa/wispconverter/config/RequestFilter.java new file mode 100644 index 00000000..aba441ff --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/config/RequestFilter.java @@ -0,0 +1,59 @@ +package it.gov.pagopa.wispconverter.config; + +import static it.gov.pagopa.wispconverter.util.Constants.HEADER_REQUEST_ID; + +import java.io.IOException; +import java.util.UUID; +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +@Slf4j +public class RequestFilter implements Filter { + + /** + * Get the request ID from the custom header "X-Request-Id" if present, otherwise it generates + * one. Set the X-Request-Id value in the {@code response} and in the MDC + * + * @param request http request + * @param response http response + * @param chain next filter + * @throws IOException if an I/O error occurs during this filter's processing of the request + * @throws ServletException if the processing fails for any other reason + */ + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + try { + HttpServletRequest httRequest = (HttpServletRequest) request; + + // get requestId from header or generate one + String requestId = httRequest.getHeader(HEADER_REQUEST_ID); + if (requestId == null || requestId.isEmpty()) { + requestId = UUID.randomUUID().toString(); + } + + // set requestId in MDC + MDC.put("requestId", requestId); + + // set requestId in the response header + ((HttpServletResponse) response).setHeader(HEADER_REQUEST_ID, requestId); + chain.doFilter(request, response); + } finally { + MDC.clear(); + } + } + +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/config/ResponseValidator.java b/src/main/java/it/gov/pagopa/wispconverter/config/ResponseValidator.java new file mode 100644 index 00000000..cbc80593 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/config/ResponseValidator.java @@ -0,0 +1,53 @@ +package it.gov.pagopa.wispconverter.config; + +import it.gov.pagopa.wispconverter.exception.AppException; + +import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.Validator; + +import org.apache.commons.lang3.StringUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class ResponseValidator { + + @Autowired + private Validator validator; + + + /** + * This method validates the response annotated with the {@link javax.validation.constraints} + * + * @param joinPoint not used + * @param result the response to validate + */ + @AfterReturning(pointcut = "execution(* it.gov.pagopa.wispconverter.controller.*.*(..))", returning = "result") + public void validateResponse(JoinPoint joinPoint, Object result) { + if (result instanceof ResponseEntity) { + validateResponse((ResponseEntity) result); + } + } + + private void validateResponse(ResponseEntity response) { + if (response.getBody() != null) { + Set> validationResults = validator.validate(response.getBody()); + + if (!validationResults.isEmpty()) { + var sb = new StringBuilder(); + for (ConstraintViolation error : validationResults) { + sb.append(error.getPropertyPath()).append(" ").append(error.getMessage()).append(". "); + } + var msg = StringUtils.chop(sb.toString()); + throw new AppException(HttpStatus.INTERNAL_SERVER_ERROR, "Invalid response", msg); + } + } + } +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/config/WebMvcConfiguration.java b/src/main/java/it/gov/pagopa/wispconverter/config/WebMvcConfiguration.java new file mode 100644 index 00000000..a953f6f5 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/config/WebMvcConfiguration.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.wispconverter.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import it.gov.pagopa.wispconverter.model.AppCorsConfiguration; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + + +@Configuration +public class WebMvcConfiguration implements WebMvcConfigurer { + + @Value("${cors.configuration}") + private String corsConfiguration; + + + @SneakyThrows + @Override + public void addCorsMappings(CorsRegistry registry) { + AppCorsConfiguration appCorsConfiguration = new ObjectMapper().readValue(corsConfiguration, + AppCorsConfiguration.class); + registry.addMapping("/**") + .allowedOrigins(appCorsConfiguration.getOrigins()) + .allowedMethods(appCorsConfiguration.getMethods()); + } +} + + diff --git a/src/main/java/it/gov/pagopa/wispconverter/controller/ConversionController.java b/src/main/java/it/gov/pagopa/wispconverter/controller/ConversionController.java new file mode 100644 index 00000000..58e3af77 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/controller/ConversionController.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.wispconverter.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ws.server.endpoint.annotation.Endpoint; +import org.springframework.ws.server.endpoint.annotation.PayloadRoot; +import org.springframework.ws.server.endpoint.annotation.RequestPayload; +import org.springframework.ws.server.endpoint.annotation.ResponsePayload; +import org.springframework.ws.soap.server.endpoint.annotation.SoapAction; + +import javax.xml.bind.JAXBElement; + +@Endpoint +@Slf4j +public class ConversionController { + + @SoapAction("nodoInviaRPT") + @PayloadRoot(localPart = "nodoInviaRPTReq") + @ResponsePayload + public JAXBElement nodoInviaRPT(@RequestPayload JAXBElement request) { + return null; + } + + @SoapAction("nodoInviaCarrelloRPT") + @PayloadRoot(localPart = "nodoInviaCarrelloRPTReq") + @ResponsePayload + public JAXBElement nodoInviaCarrelloRPT(@RequestPayload JAXBElement request) { + return null; + } + +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/controller/HomeController.java b/src/main/java/it/gov/pagopa/wispconverter/controller/HomeController.java new file mode 100644 index 00000000..6c965dce --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/controller/HomeController.java @@ -0,0 +1,71 @@ +package it.gov.pagopa.wispconverter.controller; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import it.gov.pagopa.wispconverter.model.AppInfo; +import it.gov.pagopa.wispconverter.model.ProblemJson; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.view.RedirectView; + +@RestController +@Validated +public class HomeController { + + @Value("${server.servlet.context-path}") + String basePath; + + @Value("${info.application.name}") + private String name; + + @Value("${info.application.version}") + private String version; + + @Value("${info.properties.environment}") + private String environment; + + + /** + * @return redirect to Swagger page documentation + */ + @Hidden + @GetMapping("") + public RedirectView home() { + if (!basePath.endsWith("/")) { + basePath += "/"; + } + return new RedirectView(basePath + "swagger-ui.html"); + } + + /** + * Health Check + * + * @return ok + */ + @Operation(summary = "Return OK if application is started", security = {@SecurityRequirement(name = "ApiKey")}, tags = {"Home"}) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "OK.", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = AppInfo.class))), + @ApiResponse(responseCode = "401", description = "Wrong or missing function key.", content = @Content(schema = @Schema())), + @ApiResponse(responseCode = "403", description = "Forbidden.", content = @Content(schema = @Schema())), + @ApiResponse(responseCode = "500", description = "Service unavailable.", content = @Content(mediaType = MediaType.APPLICATION_JSON_VALUE, schema = @Schema(implementation = ProblemJson.class)))}) + @GetMapping("/info") + public ResponseEntity healthCheck() { + AppInfo info = AppInfo.builder() + .name(name) + .version(version) + .environment(environment) + .build(); + return ResponseEntity.status(HttpStatus.OK).body(info); + } + +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/exception/AppError.java b/src/main/java/it/gov/pagopa/wispconverter/exception/AppError.java new file mode 100644 index 00000000..b28a99ac --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/exception/AppError.java @@ -0,0 +1,30 @@ +package it.gov.pagopa.wispconverter.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + + +@Getter +public enum AppError { + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", "Something was wrong"), + BAD_REQUEST(HttpStatus.INTERNAL_SERVER_ERROR, "Bad Request", "%s"), + UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "Unauthorized", "Error during authentication"), + FORBIDDEN(HttpStatus.FORBIDDEN, "Forbidden", "This method is forbidden"), + RESPONSE_NOT_READABLE(HttpStatus.BAD_GATEWAY, "Response Not Readable", "The response body is not readable"), + + UNKNOWN(null, null, null); + + + public final HttpStatus httpStatus; + public final String title; + public final String details; + + + AppError(HttpStatus httpStatus, String title, String details) { + this.httpStatus = httpStatus; + this.title = title; + this.details = details; + } +} + + diff --git a/src/main/java/it/gov/pagopa/wispconverter/exception/AppException.java b/src/main/java/it/gov/pagopa/wispconverter/exception/AppException.java new file mode 100644 index 00000000..a638c217 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/exception/AppException.java @@ -0,0 +1,88 @@ +package it.gov.pagopa.wispconverter.exception; + +import java.util.Formatter; +import javax.validation.constraints.NotNull; + +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.springframework.http.HttpStatus; +import org.springframework.validation.annotation.Validated; + +/** + * Custom exception. + *

See {@link ErrorHandler} + */ +@EqualsAndHashCode(callSuper = true) +@Value +@Validated +public class AppException extends RuntimeException { + + /** + * title returned to the response when this exception occurred + */ + String title; + + /** + * http status returned to the response when this exception occurred + */ + HttpStatus httpStatus; + + /** + * @param httpStatus HTTP status returned to the response + * @param title title returned to the response when this exception occurred + * @param message the detail message returend to the response + * @param cause The cause of this {@link AppException} + */ + public AppException(@NotNull HttpStatus httpStatus, @NotNull String title, + @NotNull String message, Throwable cause) { + super(message, cause); + this.title = title; + this.httpStatus = httpStatus; + } + + /** + * @param httpStatus HTTP status returned to the response + * @param title title returned to the response when this exception occurred + * @param message the detail message returend to the response + */ + public AppException(@NotNull HttpStatus httpStatus, @NotNull String title, @NotNull String message) { + super(message); + this.title = title; + this.httpStatus = httpStatus; + } + + + /** + * @param appError Response template returned to the response + * @param args {@link Formatter} replaces the placeholders in "details" string of + * {@link AppError} with the arguments. If there are more arguments than format + * specifiers, the extra arguments are ignored. + */ + public AppException(@NotNull AppError appError, Object... args) { + super(formatDetails(appError, args)); + this.httpStatus = appError.httpStatus; + this.title = appError.title; + } + + /** + * @param appError Response template returned to the response + * @param cause The cause of this {@link AppException} + * @param args Arguments for the details of {@link AppError} replaced by the + * {@link Formatter}. If there are more arguments than format specifiers, the + * extra arguments are ignored. + */ + public AppException(@NotNull AppError appError, Throwable cause, Object... args) { + super(formatDetails(appError, args), cause); + this.httpStatus = appError.httpStatus; + this.title = appError.title; + } + + private static String formatDetails(AppError appError, Object[] args) { + return String.format(appError.details, args); + } + + @Override + public String toString() { + return "AppException(" + httpStatus + ", " + title + ")" + super.toString(); + } +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/exception/ErrorHandler.java b/src/main/java/it/gov/pagopa/wispconverter/exception/ErrorHandler.java new file mode 100644 index 00000000..ce0ec7bf --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/exception/ErrorHandler.java @@ -0,0 +1,218 @@ +package it.gov.pagopa.wispconverter.exception; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import feign.FeignException; +import it.gov.pagopa.wispconverter.model.ProblemJson; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.TypeMismatchException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.nio.charset.StandardCharsets; + +/** + * All Exceptions are handled by this class + */ +@ControllerAdvice +@Slf4j +public class ErrorHandler extends ResponseEntityExceptionHandler { + + /** + * Handle if the input request is not a valid JSON + * + * @param ex {@link HttpMessageNotReadableException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + public ResponseEntity handleHttpMessageNotReadable(HttpMessageNotReadableException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + log.warn("Input not readable: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail("Invalid input format") + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handle if missing some request parameters in the request + * + * @param ex {@link MissingServletRequestParameterException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + public ResponseEntity handleMissingServletRequestParameter( + MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, + WebRequest request) { + log.warn("Missing request parameter: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + /** + * Customize the response for TypeMismatchException. + * + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return a {@code ResponseEntity} instance + */ + @Override + protected ResponseEntity handleTypeMismatch(TypeMismatchException ex, HttpHeaders headers, + HttpStatus status, WebRequest request) { + log.warn("Type mismatch: ", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(String.format("Invalid value %s for property %s", ex.getValue(), + ((MethodArgumentTypeMismatchException) ex).getName())) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + /** + * Handle if validation constraints are unsatisfied + * + * @param ex {@link MethodArgumentNotValidException} exception raised + * @param headers of the response + * @param status of the response + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with a 400 as HTTP status + */ + @Override + protected ResponseEntity handleMethodArgumentNotValid(MethodArgumentNotValidException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + List details = new ArrayList<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + details.add(error.getField() + ": " + error.getDefaultMessage()); + } + var detailsMessage = String.join(", ", details); + log.warn("Input not valid: " + detailsMessage); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(detailsMessage) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler({javax.validation.ConstraintViolationException.class}) + public ResponseEntity handleConstraintViolationException( + final javax.validation.ConstraintViolationException ex, final WebRequest request) { + log.warn("Validation Error raised:", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.BAD_REQUEST.value()) + .title(AppError.BAD_REQUEST.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST); + } + + + /** + * Handle if a {@link FeignException} is raised + * + * @param ex {@link FeignException} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status + */ + @ExceptionHandler({FeignException.class}) + public ResponseEntity handleFeignException(final FeignException ex, final WebRequest request) { + log.warn("FeignException raised: ", ex); + + ProblemJson problem; + if (ex.responseBody().isPresent()) { + var body = new String(ex.responseBody().get().array(), StandardCharsets.UTF_8); + try { + problem = new ObjectMapper().readValue(body, ProblemJson.class); + } catch (JsonProcessingException e) { + problem = ProblemJson.builder() + .status(HttpStatus.BAD_GATEWAY.value()) + .title(AppError.RESPONSE_NOT_READABLE.getTitle()) + .detail(AppError.RESPONSE_NOT_READABLE.getDetails()) + .build(); + } + } else { + problem = ProblemJson.builder() + .status(HttpStatus.BAD_GATEWAY.value()) + .title("No Response Body") + .detail("Error with external dependency") + .build(); + } + + return new ResponseEntity<>(problem, HttpStatus.valueOf(problem.getStatus())); + } + + + /** + * Handle if a {@link AppException} is raised + * + * @param ex {@link AppException} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with an appropriated HTTP status + */ + @ExceptionHandler({AppException.class}) + public ResponseEntity handleAppException(final AppException ex, + final WebRequest request) { + if (ex.getCause() != null) { + log.warn("App Exception raised: " + ex.getMessage() + "\nCause of the App Exception: ", + ex.getCause()); + } else { + log.warn("App Exception raised: ", ex); + } + var errorResponse = ProblemJson.builder() + .status(ex.getHttpStatus().value()) + .title(ex.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, ex.getHttpStatus()); + } + + + /** + * Handle if a {@link Exception} is raised + * + * @param ex {@link Exception} exception raised + * @param request from frontend + * @return a {@link ProblemJson} as response with the cause and with 500 as HTTP status + */ + @ExceptionHandler({Exception.class}) + public ResponseEntity handleGenericException(final Exception ex, + final WebRequest request) { + log.error("Generic Exception raised:", ex); + var errorResponse = ProblemJson.builder() + .status(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .title(AppError.INTERNAL_SERVER_ERROR.getTitle()) + .detail(ex.getMessage()) + .build(); + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java b/src/main/java/it/gov/pagopa/wispconverter/model/AppCorsConfiguration.java similarity index 83% rename from src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java rename to src/main/java/it/gov/pagopa/wispconverter/model/AppCorsConfiguration.java index 8d99b89f..94c54523 100644 --- a/src/main/java/it/gov/pagopa/microservice/model/AppCorsConfiguration.java +++ b/src/main/java/it/gov/pagopa/wispconverter/model/AppCorsConfiguration.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.model; +package it.gov.pagopa.wispconverter.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; @@ -18,6 +18,6 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class AppCorsConfiguration { - private String[] origins; - private String[] methods; + private String[] origins; + private String[] methods; } diff --git a/src/main/java/it/gov/pagopa/microservice/model/AppInfo.java b/src/main/java/it/gov/pagopa/wispconverter/model/AppInfo.java similarity index 76% rename from src/main/java/it/gov/pagopa/microservice/model/AppInfo.java rename to src/main/java/it/gov/pagopa/wispconverter/model/AppInfo.java index d381de02..0d94f8a8 100644 --- a/src/main/java/it/gov/pagopa/microservice/model/AppInfo.java +++ b/src/main/java/it/gov/pagopa/wispconverter/model/AppInfo.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.model; +package it.gov.pagopa.wispconverter.model; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AccessLevel; @@ -16,7 +16,7 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class AppInfo { - private String name; - private String version; - private String environment; + private String name; + private String version; + private String environment; } diff --git a/src/main/java/it/gov/pagopa/wispconverter/model/ProblemJson.java b/src/main/java/it/gov/pagopa/wispconverter/model/ProblemJson.java new file mode 100644 index 00000000..7b28fbd8 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/model/ProblemJson.java @@ -0,0 +1,42 @@ +package it.gov.pagopa.wispconverter.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * Object returned as response in case of an error. + *

See {@link it.gov.pagopa.wispconverter.exception.ErrorHandler} + */ +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +@ToString +@JsonIgnoreProperties(ignoreUnknown = true) +public class ProblemJson { + + @JsonProperty("title") + @Schema(description = "A short, summary of the problem type. Written in english and readable for engineers (usually not suited for non technical stakeholders and not localized); example: Service Unavailable") + private String title; + + @JsonProperty("status") + @Schema(example = "200", description = "The HTTP status code generated by the origin server for this occurrence of the problem.") + @Min(100) + @Max(600) + private Integer status; + + @JsonProperty("detail") + @Schema(example = "There was an error processing the request", description = "A human readable explanation specific to this occurrence of the problem.") + private String detail; + +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/repository/CacheRepository.java b/src/main/java/it/gov/pagopa/wispconverter/repository/CacheRepository.java new file mode 100644 index 00000000..bb625ef2 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/repository/CacheRepository.java @@ -0,0 +1,17 @@ +package it.gov.pagopa.wispconverter.repository; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class CacheRepository { + + @Autowired + @Qualifier("object") + private RedisTemplate template; + +} \ No newline at end of file diff --git a/src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java b/src/main/java/it/gov/pagopa/wispconverter/util/CommonUtility.java similarity index 97% rename from src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java rename to src/main/java/it/gov/pagopa/wispconverter/util/CommonUtility.java index f080c470..7454567b 100644 --- a/src/main/java/it/gov/pagopa/microservice/util/CommonUtility.java +++ b/src/main/java/it/gov/pagopa/wispconverter/util/CommonUtility.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice.util; +package it.gov.pagopa.wispconverter.util; import lombok.AccessLevel; import lombok.NoArgsConstructor; diff --git a/src/main/java/it/gov/pagopa/wispconverter/util/Constants.java b/src/main/java/it/gov/pagopa/wispconverter/util/Constants.java new file mode 100644 index 00000000..77809473 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/util/Constants.java @@ -0,0 +1,11 @@ +package it.gov.pagopa.wispconverter.util; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class Constants { + + + public static final String HEADER_REQUEST_ID = "X-Request-Id"; + +} diff --git a/src/main/java/it/gov/pagopa/wispconverter/util/OpenAPITableMetadata.java b/src/main/java/it/gov/pagopa/wispconverter/util/OpenAPITableMetadata.java new file mode 100644 index 00000000..28640385 --- /dev/null +++ b/src/main/java/it/gov/pagopa/wispconverter/util/OpenAPITableMetadata.java @@ -0,0 +1,94 @@ +package it.gov.pagopa.wispconverter.util; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface OpenAPITableMetadata { + + /** + * The field that define if the API is used for internal communications. + */ + boolean internal() default true; + + /** + * The field that define if the API is exposed to public uses. + */ + boolean external() default true; + + /** + * The field that define if the communication with the API can be executed in synchronous mode. + */ + APISynchronism synchronism() default APISynchronism.SYNC; + + /** + * The field that define if the API is secured with an authorization strategy. + */ + APISecurityMode authorization() default APISecurityMode.NONE; + + /** + * The field that define if the API is secured with an authentication strategy. + */ + APISecurityMode authentication() default APISecurityMode.NONE; + + /** + * The field that define the estimated number of invocation per second for this API. + */ + float tps() default 1; + + /** + * The field that define if the API is idempotent. + */ + boolean idempotency() default true; + + /** + * The field that define if the API operates without volatile state. + */ + boolean stateless() default true; + + /** + * The field that define what kind of operation the API executes. + */ + ReadWrite readWriteIntense() default ReadWrite.READ; + + /** + * The field that define if the API is cacheable. + */ + boolean cacheable() default false; + + @Getter + @AllArgsConstructor + enum ReadWrite { + NONE(""), + READ("Read"), + WRITE("Write"), + BOTH("Read and Write"); + + public final String value; + } + + @Getter + @AllArgsConstructor + enum APISecurityMode { + NONE("N"), + APIKEY("Y (Subscription Key)"), + JWT("Y (JWT Token)"); + + public final String value; + } + + @Getter + @AllArgsConstructor + enum APISynchronism { + SYNC("Synchronous"), + ASYNC("Asynchronous"); + + public final String value; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 5ad4771d..aa1fca24 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,7 +14,6 @@ management.health.readinessState.enabled=true springdoc.writer-with-order-by-keys=true springdoc.writer-with-default-pretty-printer=true # Server -# TODO: set your base path server.servlet.context-path=/ server.port=8080 # Logging @@ -22,4 +21,8 @@ logging.level.root=${DEFAULT_LOGGING_LEVEL:INFO} logging.level.it.gov.pagopa=${APP_LOGGING_LEVEL:INFO} # CORS configuration cors.configuration=${CORS_CONFIGURATION:'{"origins": ["*"], "methods": ["*"]}'} +# Application properties +spring.redis.host=${REDIS_HOST} +spring.redis.port=${REDIS_PORT} +spring.redis.password=${REDIS_PASSWORD} diff --git a/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java b/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java deleted file mode 100644 index 07f521b6..00000000 --- a/src/test/java/it/gov/pagopa/microservice/OpenApiGenerationTest.java +++ /dev/null @@ -1,47 +0,0 @@ -package it.gov.pagopa.microservice; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; - -import com.fasterxml.jackson.databind.ObjectMapper; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; - -@SpringBootTest(classes = Application.class) -@AutoConfigureMockMvc -class OpenApiGenerationTest { - - @Autowired ObjectMapper objectMapper; - - @Autowired private MockMvc mvc; - - @Test - void swaggerSpringPlugin() throws Exception { - mvc.perform(MockMvcRequestBuilders.get("/v3/api-docs").accept(MediaType.APPLICATION_JSON)) - .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) - .andDo( - (result) -> { - assertNotNull(result); - assertNotNull(result.getResponse()); - final String content = result.getResponse().getContentAsString(); - assertFalse(content.isBlank()); - assertFalse(content.contains("${"), "Generated swagger contains placeholders"); - Object swagger = - objectMapper.readValue(result.getResponse().getContentAsString(), Object.class); - String formatted = - objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(swagger); - Path basePath = Paths.get("openapi/"); - Files.createDirectories(basePath); - Files.write(basePath.resolve("openapi.json"), formatted.getBytes()); - }); - } -} diff --git a/src/test/java/it/gov/pagopa/microservice/ApplicationTest.java b/src/test/java/it/gov/pagopa/wispconverter/ApplicationTest.java similarity index 56% rename from src/test/java/it/gov/pagopa/microservice/ApplicationTest.java rename to src/test/java/it/gov/pagopa/wispconverter/ApplicationTest.java index 80c6e10d..a32a222b 100644 --- a/src/test/java/it/gov/pagopa/microservice/ApplicationTest.java +++ b/src/test/java/it/gov/pagopa/wispconverter/ApplicationTest.java @@ -1,4 +1,4 @@ -package it.gov.pagopa.microservice; +package it.gov.pagopa.wispconverter; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -8,9 +8,9 @@ @SpringBootTest class ApplicationTest { - @Test - void contextLoads() { - // check only if the context is loaded - assertTrue(true); - } + @Test + void contextLoads() { + // check only if the context is loaded + assertTrue(true); + } } diff --git a/src/test/java/it/gov/pagopa/wispconverter/OpenApiGenerationTest.java b/src/test/java/it/gov/pagopa/wispconverter/OpenApiGenerationTest.java new file mode 100644 index 00000000..9a1e2334 --- /dev/null +++ b/src/test/java/it/gov/pagopa/wispconverter/OpenApiGenerationTest.java @@ -0,0 +1,51 @@ +package it.gov.pagopa.wispconverter; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@SpringBootTest(classes = Application.class) +@AutoConfigureMockMvc +class OpenApiGenerationTest { + + @Autowired + ObjectMapper objectMapper; + + @Autowired + private MockMvc mvc; + + @Test + void swaggerSpringPlugin() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/v3/api-docs").accept(MediaType.APPLICATION_JSON)) + .andExpect(MockMvcResultMatchers.status().is2xxSuccessful()) + .andDo( + (result) -> { + assertNotNull(result); + assertNotNull(result.getResponse()); + final String content = result.getResponse().getContentAsString(); + assertFalse(content.isBlank()); + assertFalse(content.contains("${"), "Generated swagger contains placeholders"); + Object swagger = + objectMapper.readValue(result.getResponse().getContentAsString(), Object.class); + String formatted = + objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(swagger); + Path basePath = Paths.get("openapi/"); + Files.createDirectories(basePath); + Files.write(basePath.resolve("openapi.json"), formatted.getBytes()); + }); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index cd4c92f8..a3810704 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,5 +1,11 @@ # Info +info.application.artifactId=@project.artifactId@ +info.application.version=@project.version@ +info.application.description=@project.description@ info.properties.environment=test +# Openapi +springdoc.writer-with-order-by-keys=true +springdoc.writer-with-default-pretty-printer=true # logging logging.level.root=INFO logging.level.it.gov.pagopa=INFO